diff --git a/README.md b/README.md index 386e871d4..085fae6b0 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ We also welcome [issues submitted on GitHub](https://github.com/Microsoft/calcul ## Roadmap For information regarding Windows Calculator plans and release schedule, please see the [Windows Calculator Roadmap](docs/Roadmap.md). +### Graphing Mode +Adding graphing calculator functionality [is on the project roadmap](https://github.com/Microsoft/calculator/issues/338) and we hope that this project can create a great end-user experience around graphing. To that end, the UI from the official in-box Windows Calculator is currently part of this repository, although the proprietary Microsoft-built graphing engine, which also drives graphing in Microsoft Mathematics and OneNote, is not. Community members can still be involved in the creation of the UI, however developer builds will not have graphing functionality due to the use of a [mock implementation of the engine](/src/MockGraphingImpl) built on top of a +[common graphing API](/src/GraphingInterfaces). + ## Diagnostic Data This project collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://go.microsoft.com/fwlink/?LinkId=521839) to learn more. diff --git a/docs/ManualTests.md b/docs/ManualTests.md index ddc38ac14..884f0aefc 100644 --- a/docs/ManualTests.md +++ b/docs/ManualTests.md @@ -260,17 +260,19 @@ Steps: 4. While in the Menu: Check the About Page *Expected: Everything in the about page fits into its window* 5. For Scientific Mode: At a Larger Scale -*Expected: All buttons are present and the up arrow is grayed out.* +*Expected: All buttons are present and the 2nd button is grayed out.* 6. For Scientific Mode: At a Smaller Scale -*Expected: All buttons are present and the up arrow is able to be toggled.* +*Expected: All buttons are present and the 2nd button is able to be toggled.* 7. For Programmer Mode: At a Any Scale -*Expected: All buttons are present and the up arrow is able to be toggled.* +*Expected: All buttons are present and the 2nd button is able to be toggled.* 8. For Converter Mode: While Scaling *Expected: The number pad and input areas move around each other gracefully.* -9. Changing Language: Open Settings app > Time & language > Region & language > Add a language > Select a Right to Left (RTL) language such as Hebrew > Install the associated files> Set it to the system default. -10. Set the system number format preference: Open a Run dialog (WIN + R) > type ‘intl.cpl’ > Enter > In the format dropdown list > Select Hebrew > Apply. -11. Initiating the change: Package has completed installing > Sign out > Sign in. (This change to the app may also require a reinstallation of the build) -12. Repeat Steps 2-6 again in a (RTL) language. +9. For Graphing Mode: While Scaling +*Expected: The number pad, graph area, and input areas move around each other gracefully.* +10. Changing Language: Open Settings app > Time & language > Region & language > Add a language > Select a Right to Left (RTL) language such as Hebrew > Install the associated files> Set it to the system default. +11. Set the system number format preference: Open a Run dialog (WIN + R) > type ‘intl.cpl’ > Enter > In the format dropdown list > Select Hebrew > Apply. +12. Initiating the change: Package has completed installing > Sign out > Sign in. (This change to the app may also require a reinstallation of the build) +13. Repeat Steps 2-6 again in a (RTL) language. *Expected: No elements fall out of intended boundaries.* @@ -302,11 +304,60 @@ Verify the following: 11. "Bin" Binary: *Expected: A B C D E F 2 3 4 5 6 7 8 9 are inactive. A maximum of 64 characters can be entered.* +**Graphing Mode Test: Verify Graphing mode functions** +Steps: +1. Launch the "Calculator" app +2. Navigate to "Graphing" Calculator +3. Enter a function of x in the input field
+*Expected: Function is plotted in the graph area. Line color matches the colored square next to the input field* +4. Select the "+" button below the function input and enter more functions in the fields that appear
+*Expected: All functions are plotted in the graph area and match the colors of the input field squares* +5. Select the colored square for any function
+*Expected: Visibility of the function in the graph is toggled off/on* +6. Select the "Zoom In", "Zoom Out", and "Reset View' buttons in the graph area
+*Expected: Both X and Y axes zoom in, out, and revert to default settings, respectively* +7. Select the Trace button, then click + drag the graph until the red square is near a graphed function
+*Expected: Closest (X, Y) coordinates of the function to the red square are displayed with a black dot to indicate the location* +8. Enter "y=mx+b" into a function input field, then select "Variables" button
+*Expected: y=x+1 function is plotted in the graph, "Variables" modal window shows two variables "m" and "b" with values set to 1.* +9. Adjust the value, minimum, maximum, and step for each variable
+*Expected: y=mx+b graph adjusts to the new values for m and b, step size changes the increments of the slider for each value* +10. Share the graph via OneNote, Outlook/Mail, Twitter, and Feedback Hub
+*Expected: Modifiable message that contains an image of the graph customized for the chosen application opens* +11. Verify Key Graph Features tab shows the correct information for the following functions:
+ *(Note: IP = Inflection Points, VA = Vertical Asymptotes, HA = Horizontal Asymptotes, OA = Oblique Asymptotes)*
+ a. **y=x**
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: ⁅y∈ℝ⁆; X/Y Intercepts: (0)/(0); Max: none; Min: none; IP: none; VA: none; HA: none; OA: none; Parity: Odd; Monotonicity: (-∞, ∞) Increasing*
+ b. **y=1/x**
+ *Expected: Domain: ⁅𝑥≠0⁆; Range: ⁅y∈ℝ\{0}⁆; X/Y Intercepts: ø/ø; Max: none; Min: none; IP: none; VA: x=0; HA: y=0; OA: none; Parity: Odd; Monotonicity: (0, ∞) Decreasing, (-∞, 0) Increasing*
+ c. **y=x^2**
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: ⁅y∈{0, ∞)⁆; X/Y Intercepts: (0)/(0); Max: none; Min: (0,0); IP: none; VA: none; HA: none; OA: none; Parity: Even; Monotonicity: (0, ∞) Increasing, (-∞, 0) Decreasing*
+ d. **y=x^3**
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: ⁅y∈ℝ⁆; X/Y Intercepts: (0)/(0); Max: none; Min: none; IP: (0,0); VA: none; HA: none; OA: none; Parity: Odd; Monotonicity: (-∞, ∞) Increasing*
+ e. **y=e^x**
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: ⁅y∈(0, ∞)⁆; X/Y Intercepts: ø/(1); Max: none; Min: none; IP: none; VA: none; HA: y=0; OA: none; Parity: none; Monotonicity: (-∞, ∞) Increasing*
+ f. **y=ln(x)**
+ *Expected: Domain: ⁅𝑥>0⁆; Range: ⁅y∈ℝ⁆; X/Y Intercepts: (1)/ø; Max: none; Min: none; IP: none; VA: x=0; HA: none; OA: none; Parity: none; Monotonicity: (0, ∞) Increasing*
+ g. **y=sin(x)**
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: ⁅𝑦∈[−1,1]⁆; X/Y Intercepts: (⁅𝜋n1,n1∈ℤ⁆)/(0); Max: ⁅(2𝜋n1+𝜋/2,1),n1∈ℤ⁆; Min: ⁅(2𝜋n1+3𝜋/2,−1),n1∈ℤ⁆; IP: ⁅(𝜋n1,0),n1∈ℤ⁆; VA: none; HA: none; OA: none; Parity: Odd; Monotonicity: ⁅(2𝜋n1+𝜋/2,2𝜋n1+3𝜋/2),n1∈ℤ⁆ Decreasing; ⁅(2𝜋n1+3𝜋/2,2𝜋n1+5𝜋/2),n1∈ℤ⁆ Increasing; Period: 2𝜋*
+ h. **y=cos(x)**
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: ⁅𝑦∈[−1,1]⁆; X/Y Intercepts: (⁅𝜋n1+𝜋/2,n1∈ℤ⁆)/(1); Max: ⁅(2𝜋n1,1),n1∈ℤ⁆; Min: ⁅(2𝜋n1+𝜋,-1),n1∈ℤ⁆; IP: ⁅(𝜋n1+𝜋/2,0),n1∈ℤ⁆; VA: none; HA: none; OA: none; Parity: Even; Monotonicity: ⁅(2𝜋n1+𝜋,2𝜋n1+2𝜋),n1∈ℤ⁆ Increasing, ⁅(2𝜋n1,2𝜋n1+𝜋),n1∈ℤ⁆ Decreasing; Period: 2𝜋*
+ i. **y=tan(x)**
+ *Expected: Domain: ⁅x≠𝜋n1+𝜋/2,∀n1∈ℤ⁆; Range: ⁅𝑦∈ℝ⁆; X/Y Intercepts: (x=𝜋n1, n1 ∈ℤ)/(0); Max: none; Min: none; IP: x=𝜋n1, n1 ∈ℤ; VA: x=𝜋n1+𝜋/2, n1∈ℤ; HA: none; OA: none; Parity: Odd; Monotonicity: ⁅(𝜋n1+𝜋/2,𝜋n1+3𝜋/2),n1∈ℤ⁆ Increasing; Period: 𝜋*
+ j. **y=sqrt(25-x^2)**
+ *Expected: Domain: ⁅x∈[-5,5]⁆; Range: ⁅𝑦∈[0,5]⁆; X/Y Intercepts: (5),(-5)/(5); Max: (0,5); Min: (-5,0) and (5,0); IP: none; VA: none; HA: none; OA: none; Parity: Even; Monotonicity: (0,5) Decreasing, (-5,0) Increasing*
+ k. **y=(-3x^2+2)/(x-1)**
+ *Expected: Domain: ⁅x≠1⁆; Range: ⁅𝑦∈(-∞, -2√3 - 6}U{2√3 -6,∞⁆; X/Y Intercepts: (-√6/3),(√6/3)/(-2); Max: ⁅(√3/3+1,-2√3−6)⁆; Min: ⁅(−√3/3+1,2√3−6)⁆; IP: none; VA: x=1; HA: none; OA: y=-3x-3; Parity: none; Monotonicity: (√3/3+1,∞) Decreasing, (1,√3/3+1,) Increasing(-√3/3+1,1), Increasing, (-∞,-√3/3+1) Decreasing*
+ l. **y=sin(sin(x))** ("too complex" error test)
+ *Expected: Domain: ⁅𝑥∈ℝ⁆; Range: Unable to calculate range for this function; X/Y Intercepts: none; Max: none; Min: none; IP: none; VA: none; HA: none; OA: none; Parity: odd; Monotonicity: Unable to determine the monotonicity of the function*
+ *These features are too complex for Calculator to calculate: Range, X Intercept, Period, Minima, Maxima, Inflection Points, Monotonicity* + m. **y=mx+b**
+ *Expected: Analysis is not supported for this function* **Date Calculation Test: Verify dates can be calculated.** Steps: -1. Launch the "Calculator" app. -2. Navigate to "Date Calculation" Calculator. +1. Launch the "Calculator" app +2. Navigate to "Date Calculation" Calculator 3. With "Difference between dates" Selected Change the various date input fields *Expected: From and To reflect dates input respectively.* @@ -332,80 +383,88 @@ Steps: 1. Launch the "Calculator" app. For All Applicable Modes verify the following (note: only 11-15 and 20 work in Always-on-Top mode): -2. Press **Alt +1** to Enter "Standard" mode +2. Press **Alt +1** to enter "Standard" mode *Expected: Move to "Standard" screen.* -3. Press **Alt +2** to Enter "Scientific" mode +3. Press **Alt +2** to enter "Scientific" mode *Expected: Move to "Scientific" screen.* -4. Press **Alt +3** to Enter "Programmer" mode +4. Press **Alt +3** to enter "Programmer" mode *Expected: Move to "Programming" screen.* -5. Press **Alt +4** to Enter "Date Calculation" mode +5. Press **Alt +4** to enter "Date Calculation" mode *Expected: Move to "Date Calculation" screen.* -6. Press **Ctrl +M** to Store in Memory -7. Press **Ctrl +P** to Add to Active Memory -8. Press **Ctrl +Q** to Subtract form Active Memory -9. Press **Ctrl +R** to Recall from Memory -10. Press **Ctrl +L** to Clear from Memory -11. Press **Delete** to Clear Current Input 'CE' -12. Press **Esc** to Full Clear Input 'C' -13. Press **F9** to Toggle '±' -14. Press **R** to Select '1/x' -15. Press **@** to Select '√' -16. Press **Ctrl + H** to Toggle History Panel +6 Press **Alt +5** to enter "Graphing" mode +*Expected: Move to "Graphing" screen.* +7. Press **Ctrl +M** to Store in Memory +8. Press **Ctrl +P** to Add to Active Memory +9. Press **Ctrl +Q** to Subtract form Active Memory +10. Press **Ctrl +R** to Recall from Memory +11. Press **Ctrl +L** to Clear from Memory +12. Press **Delete** to Clear Current Input 'CE' +13. Press **Esc** to Full Clear Input 'C' +14. Press **F9** to Toggle '±' +15. Press **R** to Select '1/x' +16. Press **@** to Select '√' +17. Press **Ctrl + H** to Toggle History Panel *Expected: Function when in small scale window.* -17. Press **Up arrow** to Move up History Panel +18. Press **Up arrow** to Move up History Panel *Expected: Function when in small scale window.* -18. Press **Down arrow** to Move Down History Panel +19. Press **Down arrow** to Move Down History Panel *Expected: Function when in small scale window.* -19. Press **Ctrl + Shift + D** to Clear History Panel +20. Press **Ctrl + Shift + D** to Clear History Panel *Expected: Function when in small scale window.* -20. Press **Spacebar** to Repeat Last Input +21. Press **Spacebar** to Repeat Last Input Verify the following in Scientific Mode -21. Press **F3** to Select 'DEG' -22. Press **F4** to Select 'RAD' -23. Press **F5** to Select 'GRAD' -24. Press **Ctrl +G** to Select '10ˣ' -25. Press **Ctrl +Y** to Select 'y√x' -26. Press **Shift +O** to Select 'sin-1' -27. Press **Shift + S** to Select 'cos-1' -28. Press **Shift +T** to Select 'tan-1' -29. Press **Ctrl +O** to Select 'Cosh' -30. Press **Ctrl +S** to Select 'Sinh' -31. Press **Ctrl +T** to Select 'Tanh' -32. Press **D** to Select 'Mod' -33. Press **L** to Select 'log' -34. Press **M** to Select 'dms' -35. Press **N** to Select 'ln' -36. Press **Ctrl +N** to Select 'ex' -37. Press **O** to Select 'Cos' -38. Press **P** to Select 'π' -39. Press **Q** to Select 'x²' -40. Press **S** to Select 'Sin' -41. Press **T** to Select 'Tan' -42. Press **V** to Toggle 'F-E' -43. Press **X** to Select 'Exp' -44. Press **Y** or **^** to Select 'xʸ' -45. Press **#** to Select 'x³' -46. Press **!** to Select 'n!' +22. Press **F3** to Select 'DEG' +23. Press **F4** to Select 'RAD' +24. Press **F5** to Select 'GRAD' +25. Press **Ctrl +G** to Select '10ˣ' +26. Press **Ctrl +Y** to Select 'y√x' +27. Press **Shift +O** to Select 'sin-1' +28. Press **Shift + S** to Select 'cos-1' +29. Press **Shift +T** to Select 'tan-1' +30. Press **Ctrl +O** to Select 'Cosh' +31. Press **Ctrl +S** to Select 'Sinh' +32. Press **Ctrl +T** to Select 'Tanh' +33. Press **D** to Select 'Mod' +34. Press **L** to Select 'log' +35. Press **M** to Select 'dms' +36. Press **N** to Select 'ln' +37. Press **Ctrl +N** to Select 'ex' +38. Press **O** to Select 'Cos' +39. Press **P** to Select 'π' +40. Press **Q** to Select 'x²' +41. Press **S** to Select 'Sin' +42. Press **T** to Select 'Tan' +43. Press **V** to Toggle 'F-E' +44. Press **X** to Select 'Exp' +45. Press **Y** or **^** to Select 'xʸ' +46. Press **#** to Select 'x³' +47. Press **!** to Select 'n!' Verify the following in Programmer Mode -47. Press **F2** to Select 'DWORD' -48. Press **F3** to Select 'WORD' -49. Press **F4** to Select 'BYTE' -50. Press **F5** to Select 'HEX' -51. Press **F6** to Select 'DEC' -52. Press **F7** to Select 'OCT' -53. Press **F8** to Select 'BIN' -54. Press **F12** to Select 'QWORD' -55. Press **A-F** to Input in HEX -56. Press **J** to Select 'RoL' -57. Press **K** to Select 'RoR' -58. Press **<** to Select 'Lsh' -59. Press **>** to Select 'Rsh' -60. Press **%** to Select 'Mod' -61. Press **|** to Select 'Or' -62. Press **~** to Select 'Not' -63. Press **&** to Select 'And' +48. Press **F2** to Select 'DWORD' +49. Press **F3** to Select 'WORD' +50. Press **F4** to Select 'BYTE' +51. Press **F5** to Select 'HEX' +52. Press **F6** to Select 'DEC' +53. Press **F7** to Select 'OCT' +54. Press **F8** to Select 'BIN' +55. Press **F12** to Select 'QWORD' +56. Press **A-F** to Input in HEX +57. Press **J** to Select 'RoL' +58. Press **K** to Select 'RoR' +59. Press **<** to Select 'Lsh' +60. Press **>** to Select 'Rsh' +61. Press **%** to Select 'Mod' +62. Press **|** to Select 'Or' +63. Press **~** to Select 'Not' +64. Press **&** to Select 'And' + + Verify the following in Graphing Mode +65. Press **x** to Select 'x' +66. Press **y** to Select 'y' +67. Press **Ctrl +[Numpad+]** to Select 'Zoom In' +68. Press **Ctrl +[Numpad-]** to Select 'Zoom Out' ## Localization Tests diff --git a/nuget.config b/nuget.config index 0286336f8..6ef7f3420 100644 --- a/nuget.config +++ b/nuget.config @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/src/CalcManager/CalcManager.vcxproj b/src/CalcManager/CalcManager.vcxproj index b30b39761..80940c6a3 100644 --- a/src/CalcManager/CalcManager.vcxproj +++ b/src/CalcManager/CalcManager.vcxproj @@ -132,29 +132,9 @@ - - false - - - false - - - false - - - false - - - false - - - false - - - false - - + false + true @@ -366,4 +346,4 @@ - \ No newline at end of file + diff --git a/src/CalcManager/CalculatorVector.h b/src/CalcManager/CalculatorVector.h new file mode 100644 index 000000000..4a139c8ea --- /dev/null +++ b/src/CalcManager/CalculatorVector.h @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include "winerror_cross_platform.h" +#include "Ratpack/CalcErr.h" +#include // for std::out_of_range +#include "sal_cross_platform.h" // for SAL + +template +class CalculatorVector +{ +public: + ResultCode GetAt(_In_opt_ unsigned int index, _Out_ TType* item) + { + try + { + *item = m_vector.at(index); + } + catch (const std::out_of_range& /*ex*/) + { + return E_BOUNDS; + } + return S_OK; + } + + ResultCode GetSize(_Out_ unsigned int* size) + { + *size = static_cast(m_vector.size()); + return S_OK; + } + + ResultCode SetAt(_In_ unsigned int index, _In_opt_ TType item) + { + try + { + m_vector[index] = item; + } + catch (const std::out_of_range& /*ex*/) + { + return E_BOUNDS; + } + return S_OK; + } + + ResultCode RemoveAt(_In_ unsigned int index) + { + if (index < m_vector.size()) + { + m_vector.erase(m_vector.begin() + index); + } + else + { + return E_BOUNDS; + } + return S_OK; + } + + ResultCode InsertAt(_In_ unsigned int index, _In_ TType item) + { + try + { + auto iter = m_vector.begin() + index; + m_vector.insert(iter, item); + } + catch (const std::bad_alloc& /*ex*/) + { + return E_OUTOFMEMORY; + } + return S_OK; + } + + ResultCode Truncate(_In_ unsigned int index) + { + if (index < m_vector.size()) + { + auto startIter = m_vector.begin() + index; + m_vector.erase(startIter, m_vector.end()); + } + else + { + return E_BOUNDS; + } + return S_OK; + } + + ResultCode Append(_In_opt_ TType item) + { + try + { + m_vector.push_back(item); + } + catch (const std::bad_alloc& /*ex*/) + { + return E_OUTOFMEMORY; + } + return S_OK; + } + + ResultCode RemoveAtEnd() + { + m_vector.erase(--(m_vector.end())); + return S_OK; + } + + ResultCode Clear() + { + m_vector.clear(); + return S_OK; + } + + ResultCode GetString(_Out_ std::wstring* expression) + { + unsigned int nTokens = 0; + ResultCode hr = this->GetSize(&nTokens); + if (SUCCEEDED(hr)) + { + + std::pair currentPair; + for (unsigned int i = 0; i < nTokens; i++) + { + hr = this->GetAt(i, ¤tPair); + if (SUCCEEDED(hr)) + { + expression->append(currentPair.first); + + if (i != (nTokens - 1)) + { + expression->append(L" "); + } + } + } + + std::wstring expressionSuffix{}; + hr = GetExpressionSuffix(&expressionSuffix); + if (SUCCEEDED(hr)) + { + expression->append(expressionSuffix); + } + } + + return hr; + } + + ResultCode GetExpressionSuffix(_Out_ std::wstring* suffix) + { + *suffix = L" ="; + return S_OK; + } + +private: + std::vector m_vector; +}; diff --git a/src/CalcViewModel/ApplicationViewModel.cpp b/src/CalcViewModel/ApplicationViewModel.cpp index efe9bca83..40141ada0 100644 --- a/src/CalcViewModel/ApplicationViewModel.cpp +++ b/src/CalcViewModel/ApplicationViewModel.cpp @@ -43,6 +43,7 @@ namespace ApplicationViewModel::ApplicationViewModel() : m_CalculatorViewModel(nullptr) , m_DateCalcViewModel(nullptr) + , m_GraphingCalcViewModel(nullptr) , m_ConverterViewModel(nullptr) , m_PreviousMode(ViewMode::None) , m_mode(ViewMode::None) @@ -132,6 +133,13 @@ void ApplicationViewModel::OnModeChanged() } m_CalculatorViewModel->SetCalculatorType(m_mode); } + else if (NavCategory::IsGraphingCalculatorViewMode(m_mode)) + { + if (!m_GraphingCalcViewModel) + { + m_GraphingCalcViewModel = ref new GraphingCalculatorViewModel(); + } + } else if (NavCategory::IsDateCalculatorViewMode(m_mode)) { if (!m_DateCalcViewModel) @@ -182,7 +190,7 @@ void ApplicationViewModel::OnCopyCommand(Object ^ parameter) { DateCalcViewModel->OnCopyCommand(parameter); } - else + else if (NavCategory::IsCalculatorViewMode(m_mode)) { CalculatorViewModel->OnCopyCommand(parameter); } diff --git a/src/CalcViewModel/ApplicationViewModel.h b/src/CalcViewModel/ApplicationViewModel.h index 8a293eac8..7e4238d8a 100644 --- a/src/CalcViewModel/ApplicationViewModel.h +++ b/src/CalcViewModel/ApplicationViewModel.h @@ -5,6 +5,7 @@ #include "StandardCalculatorViewModel.h" #include "DateCalculatorViewModel.h" +#include "GraphingCalculator/GraphingCalculatorViewModel.h" #include "UnitConverterViewModel.h" namespace CalculatorApp @@ -21,6 +22,7 @@ namespace CalculatorApp OBSERVABLE_OBJECT(); OBSERVABLE_PROPERTY_RW(StandardCalculatorViewModel ^, CalculatorViewModel); OBSERVABLE_PROPERTY_RW(DateCalculatorViewModel ^, DateCalcViewModel); + OBSERVABLE_PROPERTY_RW(GraphingCalculatorViewModel ^, GraphingCalcViewModel); OBSERVABLE_PROPERTY_RW(UnitConverterViewModel ^, ConverterViewModel); OBSERVABLE_PROPERTY_RW(CalculatorApp::Common::ViewMode, PreviousMode); OBSERVABLE_PROPERTY_R(bool, IsAlwaysOnTop); diff --git a/src/CalcViewModel/CalcViewModel.vcxproj b/src/CalcViewModel/CalcViewModel.vcxproj index c00baff7f..db212ad0a 100644 --- a/src/CalcViewModel/CalcViewModel.vcxproj +++ b/src/CalcViewModel/CalcViewModel.vcxproj @@ -122,29 +122,9 @@ - - false - - - false - - - false - - - false - - - false - - - false - - - false - - + false + true @@ -343,6 +323,11 @@ + + + + + @@ -372,6 +357,9 @@ + + + @@ -392,6 +380,9 @@ {311e866d-8b93-4609-a691-265941fee101} + + {e727a92b-f149-492c-8117-c039a298719b} + diff --git a/src/CalcViewModel/CalcViewModel.vcxproj.filters b/src/CalcViewModel/CalcViewModel.vcxproj.filters index 051808a65..0b3507fe3 100644 --- a/src/CalcViewModel/CalcViewModel.vcxproj.filters +++ b/src/CalcViewModel/CalcViewModel.vcxproj.filters @@ -10,6 +10,9 @@ {0184f727-b8aa-4af8-a699-63f1b56e7853} + + {cf7dca32-9727-4f98-83c3-1c0ca7dd1e0c} + @@ -71,9 +74,18 @@ DataLoaders + + GraphingCalculator + + + GraphingCalculator + Common\Automation + + GraphingCalculator + @@ -172,8 +184,23 @@ Common + + GraphingCalculator + + + GraphingCalculator + + + Common + + + GraphingCalculator + + + GraphingCalculator + - Common + Common diff --git a/src/CalcViewModel/Common/Automation/INarratorAnnouncementHost.h b/src/CalcViewModel/Common/Automation/INarratorAnnouncementHost.h new file mode 100644 index 000000000..9f947f41a --- /dev/null +++ b/src/CalcViewModel/Common/Automation/INarratorAnnouncementHost.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "NarratorAnnouncement.h" + +// Declaration of the INarratorAnnouncementHost interface. +// This interface exists to hide the concrete announcement host +// being used. Depending on the version of the OS the app is running on, +// the app may need a host that uses LiveRegionChanged or RaiseNotification. + +namespace CalculatorApp::Common::Automation +{ +public + interface class INarratorAnnouncementHost + { + public: + // Is the host available on this OS. + bool IsHostAvailable(); + + // Make a new instance of a concrete host. + INarratorAnnouncementHost ^ MakeHost(); + + // Make an announcement using the concrete host's preferred method. + void Announce(NarratorAnnouncement ^ announcement); + }; +} diff --git a/src/CalcViewModel/Common/Automation/LiveRegionHost.cpp b/src/CalcViewModel/Common/Automation/LiveRegionHost.cpp new file mode 100644 index 000000000..0df3a96c6 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/LiveRegionHost.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "LiveRegionHost.h" + +using namespace CalculatorApp::Common::Automation; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Automation::Peers; +using namespace Windows::UI::Xaml::Controls; + +LiveRegionHost::LiveRegionHost() + : m_host(nullptr) +{ +} + +bool LiveRegionHost::IsHostAvailable() +{ + // LiveRegion is always available. + return true; +} + +INarratorAnnouncementHost ^ LiveRegionHost::MakeHost() +{ + return ref new LiveRegionHost(); +} + +void LiveRegionHost::Announce(NarratorAnnouncement ^ announcement) +{ + if (m_host == nullptr) + { + m_host = ref new TextBlock(); + AutomationProperties::SetLiveSetting(m_host, AutomationLiveSetting::Assertive); + } + + AutomationProperties::SetName(m_host, announcement->Announcement); + AutomationPeer ^ peer = FrameworkElementAutomationPeer::FromElement(m_host); + if (peer != nullptr) + { + peer->RaiseAutomationEvent(AutomationEvents::LiveRegionChanged); + } +} diff --git a/src/CalcViewModel/Common/Automation/LiveRegionHost.h b/src/CalcViewModel/Common/Automation/LiveRegionHost.h new file mode 100644 index 000000000..fef7c7146 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/LiveRegionHost.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "INarratorAnnouncementHost.h" + +// Declaration of the LiveRegionHost class. +// This class announces NarratorAnnouncements using the LiveRegionChanged event. +// This event is unreliable and should be deprecated in favor of the new +// RaiseNotification API in RS3. + +namespace CalculatorApp::Common::Automation +{ + // This class exists so that the app can run on RS2 and use LiveRegions + // to host notifications on those builds. + // When the app switches to min version RS3, this class can be removed + // and the app will switch to using the Notification API. + // TODO - MSFT 12735088 +public + ref class LiveRegionHost sealed : public INarratorAnnouncementHost + { + public: + LiveRegionHost(); + + virtual bool IsHostAvailable(); + virtual INarratorAnnouncementHost ^ MakeHost(); + + virtual void Announce(NarratorAnnouncement ^ announcement); + + private: + Windows::UI::Xaml::UIElement ^ m_host; + }; +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp index 940c88c9c..15fca2c6e 100644 --- a/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp @@ -23,6 +23,7 @@ namespace CalculatorApp::Common::Automation StringReference DisplayCopied(L"DisplayCopied"); StringReference OpenParenthesisCountChanged(L"OpenParenthesisCountChanged"); StringReference NoParenthesisAdded(L"NoParenthesisAdded"); + StringReference GraphModeChanged(L"GraphModeChanged"); } } @@ -140,3 +141,12 @@ NarratorAnnouncement ^ CalculatorAnnouncement::GetNoRightParenthesisAddedAnnounc AutomationNotificationKind::ActionCompleted, AutomationNotificationProcessing::ImportantMostRecent); } + +NarratorAnnouncement ^ CalculatorAnnouncement::GetGraphModeChangedAnnouncement(Platform::String ^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::GraphModeChanged, + AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::ImportantMostRecent); +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h index fef7e2609..81c3856b4 100644 --- a/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h @@ -66,5 +66,8 @@ public static NarratorAnnouncement ^ GetOpenParenthesisCountChangedAnnouncement(Platform::String ^ announcement); static NarratorAnnouncement ^ GetNoRightParenthesisAddedAnnouncement(Platform::String ^ announcement); + + static NarratorAnnouncement ^ GetGraphModeChangedAnnouncement(Platform::String ^ announcement); + }; } diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.cpp b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.cpp new file mode 100644 index 000000000..a103c7e71 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.cpp @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NarratorAnnouncementHostFactory.h" +#include "NotificationHost.h" +#include "LiveRegionHost.h" + +using namespace CalculatorApp::Common::Automation; +using namespace std; + +INarratorAnnouncementHost ^ NarratorAnnouncementHostFactory::s_hostProducer; +vector NarratorAnnouncementHostFactory::s_hosts; + +// This static variable is used only to call the initialization function, to initialize the other static variables. +int NarratorAnnouncementHostFactory::s_init = NarratorAnnouncementHostFactory::Initialize(); +int NarratorAnnouncementHostFactory::Initialize() +{ + RegisterHosts(); + NarratorAnnouncementHostFactory::s_hostProducer = GetHostProducer(); + + return 0; +} + +// For now, there are two type of announcement hosts. +// We'd prefer to use Notification if it's available and fall back to LiveRegion +// if not. The availability of the host depends on the version of the OS the app is running on. +// When the app switches to min version RS3, the LiveRegionHost can be removed and we will always +// use NotificationHost. +// TODO - MSFT 12735088 +void NarratorAnnouncementHostFactory::RegisterHosts() +{ + // The host that will be used is the first available host, + // therefore, order of hosts is important here. + NarratorAnnouncementHostFactory::s_hosts = { ref new NotificationHost(), ref new LiveRegionHost() }; +} + +INarratorAnnouncementHost ^ NarratorAnnouncementHostFactory::GetHostProducer() +{ + for (INarratorAnnouncementHost ^ host : NarratorAnnouncementHostFactory::s_hosts) + { + if (host->IsHostAvailable()) + { + return host; + } + } + + assert(false && L"No suitable AnnouncementHost was found."); + return nullptr; +} + +INarratorAnnouncementHost ^ NarratorAnnouncementHostFactory::MakeHost() +{ + if (NarratorAnnouncementHostFactory::s_hostProducer == nullptr) + { + assert(false && L"No host producer has been assigned."); + return nullptr; + } + + return NarratorAnnouncementHostFactory::s_hostProducer->MakeHost(); +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.h b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.h new file mode 100644 index 000000000..4b739a799 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "INarratorAnnouncementHost.h" + +// Declaration of the NarratorAnnouncementHostFactory class. +// This class exists to hide the construction of a concrete INarratorAnnouncementHost. +// Depending on the version of the OS the app is running on, the factory will return +// an announcement host appropriate for that version. + +namespace CalculatorApp::Common::Automation +{ + class NarratorAnnouncementHostFactory + { + public: + static INarratorAnnouncementHost ^ MakeHost(); + + private: + NarratorAnnouncementHostFactory() + { + } + + static int Initialize(); + static void RegisterHosts(); + static INarratorAnnouncementHost ^ GetHostProducer(); + + private: + static int s_init; + static INarratorAnnouncementHost ^ s_hostProducer; + static std::vector s_hosts; + }; +} diff --git a/src/CalcViewModel/Common/Automation/NotificationHost.cpp b/src/CalcViewModel/Common/Automation/NotificationHost.cpp new file mode 100644 index 000000000..92bf846ef --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NotificationHost.cpp @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NotificationHost.h" + +using namespace CalculatorApp::Common::Automation; +using namespace Windows::Foundation::Metadata; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Automation::Peers; +using namespace Windows::UI::Xaml::Controls; + +NotificationHost::NotificationHost() + : m_host(nullptr) +{ +} + +bool NotificationHost::IsHostAvailable() +{ + return ApiInformation::IsMethodPresent(L"Windows.UI.Xaml.Automation.Peers.AutomationPeer", L"RaiseNotificationEvent"); +} + +INarratorAnnouncementHost ^ NotificationHost::MakeHost() +{ + return ref new NotificationHost(); +} + +void NotificationHost::Announce(NarratorAnnouncement ^ announcement) +{ + if (m_host == nullptr) + { + m_host = ref new TextBlock(); + } + + auto peer = FrameworkElementAutomationPeer::FromElement(m_host); + if (peer != nullptr) + { + peer->RaiseNotificationEvent( + GetWindowsNotificationKind(announcement->Kind), + GetWindowsNotificationProcessing(announcement->Processing), + announcement->Announcement, + announcement->ActivityId); + } +} + +StandardPeers::AutomationNotificationKind NotificationHost::GetWindowsNotificationKind(CustomPeers::AutomationNotificationKind customKindType) +{ + switch (customKindType) + { + case CustomPeers::AutomationNotificationKind::ItemAdded: + return StandardPeers::AutomationNotificationKind::ItemAdded; + + case CustomPeers::AutomationNotificationKind::ItemRemoved: + return StandardPeers::AutomationNotificationKind::ItemRemoved; + + case CustomPeers::AutomationNotificationKind::ActionCompleted: + return StandardPeers::AutomationNotificationKind::ActionCompleted; + + case CustomPeers::AutomationNotificationKind::ActionAborted: + return StandardPeers::AutomationNotificationKind::ActionAborted; + + case CustomPeers::AutomationNotificationKind::Other: + return StandardPeers::AutomationNotificationKind::Other; + + default: + assert(false && L"Unexpected AutomationNotificationKind"); + } + + return StandardPeers::AutomationNotificationKind::Other; +} + +StandardPeers::AutomationNotificationProcessing +NotificationHost::GetWindowsNotificationProcessing(CustomPeers::AutomationNotificationProcessing customProcessingType) +{ + switch (customProcessingType) + { + case CustomPeers::AutomationNotificationProcessing::ImportantAll: + return StandardPeers::AutomationNotificationProcessing::ImportantAll; + + case CustomPeers::AutomationNotificationProcessing::ImportantMostRecent: + return StandardPeers::AutomationNotificationProcessing::ImportantMostRecent; + + case CustomPeers::AutomationNotificationProcessing::All: + return StandardPeers::AutomationNotificationProcessing::All; + + case CustomPeers::AutomationNotificationProcessing::MostRecent: + return StandardPeers::AutomationNotificationProcessing::MostRecent; + + case CustomPeers::AutomationNotificationProcessing::CurrentThenMostRecent: + return StandardPeers::AutomationNotificationProcessing::CurrentThenMostRecent; + + default: + assert(false && L"Unexpected AutomationNotificationProcessing"); + } + + return StandardPeers::AutomationNotificationProcessing::ImportantMostRecent; +} diff --git a/src/CalcViewModel/Common/Automation/NotificationHost.h b/src/CalcViewModel/Common/Automation/NotificationHost.h new file mode 100644 index 000000000..d0a929c64 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NotificationHost.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "INarratorAnnouncementHost.h" + +// Declaration of the NotificationHost class. +// This class announces NarratorAnnouncements using the RaiseNotification API +// available in RS3. + +namespace CalculatorApp::Common::Automation +{ +public + ref class NotificationHost sealed : public INarratorAnnouncementHost + { + public: + NotificationHost(); + + virtual bool IsHostAvailable(); + virtual INarratorAnnouncementHost ^ MakeHost(); + + virtual void Announce(NarratorAnnouncement ^ announcement); + + private: + static Windows::UI::Xaml::Automation::Peers::AutomationNotificationKind + GetWindowsNotificationKind(CalculatorApp::Common::Automation::AutomationNotificationKind customKindType); + + static Windows::UI::Xaml::Automation::Peers::AutomationNotificationProcessing + GetWindowsNotificationProcessing(CalculatorApp::Common::Automation::AutomationNotificationProcessing customProcessingType); + + private: + Windows::UI::Xaml::UIElement ^ m_host; + }; +} diff --git a/src/CalcViewModel/Common/CalculatorButtonUser.h b/src/CalcViewModel/Common/CalculatorButtonUser.h index 113d34dc8..8afd0fe9a 100644 --- a/src/CalcViewModel/Common/CalculatorButtonUser.h +++ b/src/CalcViewModel/Common/CalculatorButtonUser.h @@ -120,7 +120,7 @@ public RshL = (int)CM::Command::CommandRSHFL, RolC = (int)CM::Command::CommandROLC, RorC = (int)CM::Command::CommandRORC, - + BINSTART = (int)CM::Command::CommandBINEDITSTART, BINPOS0 = (int)CM::Command::CommandBINPOS0, BINPOS1 = (int)CM::Command::CommandBINPOS1, @@ -194,6 +194,14 @@ public MemoryRecall = (int)CM::Command::CommandRECALL, MemoryClear = (int)CM::Command::CommandMCLEAR, BitflipButton = 1000, - FullKeypadButton = 1001 + FullKeypadButton = 1001, + + // Buttons used in graphing calculator + LessThan, + LessThanOrEqualTo, + GreaterThan, + GreaterThanOrEqualTo, + X, + Y }; } diff --git a/src/CalcViewModel/Common/NavCategory.cpp b/src/CalcViewModel/Common/NavCategory.cpp index 90f25c928..a30287132 100644 --- a/src/CalcViewModel/Common/NavCategory.cpp +++ b/src/CalcViewModel/Common/NavCategory.cpp @@ -5,6 +5,7 @@ #include "NavCategory.h" #include "AppResourceProvider.h" #include "Common/LocalizationStringUtil.h" +#include using namespace CalculatorApp; using namespace CalculatorApp::Common; @@ -22,12 +23,6 @@ static constexpr bool SUPPORTS_ALL = true; static constexpr bool SUPPORTS_NEGATIVE = true; static constexpr bool POSITIVE_ONLY = false; -// The order of items in this list determines the order of groups in the menu. -static constexpr array s_categoryGroupManifest = { - NavCategoryGroupInitializer{ CategoryGroupType::Calculator, L"CalculatorModeTextCaps", L"CalculatorModeText", L"CalculatorModePluralText" }, - NavCategoryGroupInitializer{ CategoryGroupType::Converter, L"ConverterModeTextCaps", L"ConverterModeText", L"ConverterModePluralText" } -}; - // vvv THESE CONSTANTS SHOULD NEVER CHANGE vvv static constexpr int STANDARD_ID = 0; static constexpr int SCIENTIFIC_ID = 1; @@ -46,145 +41,266 @@ static constexpr int DATA_ID = 13; static constexpr int PRESSURE_ID = 14; static constexpr int ANGLE_ID = 15; static constexpr int CURRENCY_ID = 16; +static constexpr int GRAPHING_ID = 17; // ^^^ THESE CONSTANTS SHOULD NEVER CHANGE ^^^ +wchar_t* towchar_t(int number) +{ + auto wstr = to_wstring(number); + return _wcsdup(wstr.c_str()); +} + +extern "C" +{ + WINADVAPI LSTATUS APIENTRY RegGetValueW( + _In_ HKEY hkey, + _In_opt_ LPCWSTR lpSubKey, + _In_opt_ LPCWSTR lpValue, + _In_ DWORD dwFlags, + _Out_opt_ LPDWORD pdwType, + _When_( + (dwFlags & 0x7F) == RRF_RT_REG_SZ || (dwFlags & 0x7F) == RRF_RT_REG_EXPAND_SZ || (dwFlags & 0x7F) == (RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ) + || *pdwType == REG_SZ || *pdwType == REG_EXPAND_SZ, + _Post_z_) _When_((dwFlags & 0x7F) == RRF_RT_REG_MULTI_SZ || *pdwType == REG_MULTI_SZ, _Post_ _NullNull_terminated_) + _Out_writes_bytes_to_opt_(*pcbData, *pcbData) PVOID pvData, + _Inout_opt_ LPDWORD pcbData); +} + +bool IsGraphingModeAvailable() +{ + static bool supportGraph = Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath"); + return supportGraph; +} + +Box ^ _isGraphingModeEnabledCached = nullptr; +bool IsGraphingModeEnabled() +{ + if (!IsGraphingModeAvailable()) + { + return false; + } + + if (_isGraphingModeEnabledCached != nullptr) + { + return _isGraphingModeEnabledCached->Value; + } + + DWORD allowGraphingCalculator{ 0 }; + DWORD bufferSize{ sizeof(allowGraphingCalculator) }; + // Make sure to call RegGetValueW only on Windows 10 1903+ + if (RegGetValueW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Calculator", + L"AllowGraphingCalculator", + RRF_RT_REG_DWORD | RRF_RT_REG_BINARY, + nullptr, + reinterpret_cast(&allowGraphingCalculator), + &bufferSize) + == ERROR_SUCCESS) + { + _isGraphingModeEnabledCached = allowGraphingCalculator != 0; + } + else + { + _isGraphingModeEnabledCached = true; + } + return _isGraphingModeEnabledCached->Value; +} + // The order of items in this list determines the order of items in the menu. -static constexpr array s_categoryManifest = { NavCategoryInitializer{ ViewMode::Standard, - STANDARD_ID, - L"Standard", - L"StandardMode", - L"\uE8EF", - CategoryGroupType::Calculator, - MyVirtualKey::Number1, - SUPPORTS_ALL }, - NavCategoryInitializer{ ViewMode::Scientific, - SCIENTIFIC_ID, - L"Scientific", - L"ScientificMode", - L"\uF196", - CategoryGroupType::Calculator, - MyVirtualKey::Number2, - SUPPORTS_ALL }, - NavCategoryInitializer{ ViewMode::Programmer, - PROGRAMMER_ID, - L"Programmer", - L"ProgrammerMode", - L"\uECCE", - CategoryGroupType::Calculator, - MyVirtualKey::Number3, - SUPPORTS_ALL }, - NavCategoryInitializer{ ViewMode::Date, - DATE_ID, - L"Date", - L"DateCalculationMode", - L"\uE787", - CategoryGroupType::Calculator, - MyVirtualKey::Number4, - SUPPORTS_ALL }, - NavCategoryInitializer{ ViewMode::Currency, - CURRENCY_ID, - L"Currency", - L"CategoryName_Currency", - L"\uEB0D", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Volume, - VOLUME_ID, - L"Volume", - L"CategoryName_Volume", - L"\uF1AA", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Length, - LENGTH_ID, - L"Length", - L"CategoryName_Length", - L"\uECC6", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Weight, - WEIGHT_ID, - L"Weight and Mass", - L"CategoryName_Weight", - L"\uF4C1", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Temperature, - TEMPERATURE_ID, - L"Temperature", - L"CategoryName_Temperature", - L"\uE7A3", - CategoryGroupType::Converter, - MyVirtualKey::None, - SUPPORTS_NEGATIVE }, - NavCategoryInitializer{ ViewMode::Energy, - ENERGY_ID, - L"Energy", - L"CategoryName_Energy", - L"\uECAD", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Area, - AREA_ID, - L"Area", - L"CategoryName_Area", - L"\uE809", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Speed, - SPEED_ID, - L"Speed", - L"CategoryName_Speed", - L"\uEADA", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Time, - TIME_ID, - L"Time", - L"CategoryName_Time", - L"\uE917", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Power, - POWER_ID, - L"Power", - L"CategoryName_Power", - L"\uE945", - CategoryGroupType::Converter, - MyVirtualKey::None, - SUPPORTS_NEGATIVE }, - NavCategoryInitializer{ ViewMode::Data, - DATA_ID, - L"Data", - L"CategoryName_Data", - L"\uF20F", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Pressure, - PRESSURE_ID, - L"Pressure", - L"CategoryName_Pressure", - L"\uEC4A", - CategoryGroupType::Converter, - MyVirtualKey::None, - POSITIVE_ONLY }, - NavCategoryInitializer{ ViewMode::Angle, - ANGLE_ID, - L"Angle", - L"CategoryName_Angle", - L"\uF515", - CategoryGroupType::Converter, - MyVirtualKey::None, - SUPPORTS_NEGATIVE } }; +static const list s_categoryManifest = [] { + auto res = list{ NavCategoryInitializer{ ViewMode::Standard, + STANDARD_ID, + L"Standard", + L"StandardMode", + L"\uE8EF", + CategoryGroupType::Calculator, + MyVirtualKey::Number1, + L"1", + SUPPORTS_ALL, + true }, + NavCategoryInitializer{ ViewMode::Scientific, + SCIENTIFIC_ID, + L"Scientific", + L"ScientificMode", + L"\uF196", + CategoryGroupType::Calculator, + MyVirtualKey::Number2, + L"2", + SUPPORTS_ALL, + true } }; + + int currentIndex = 3; + bool supportGraphingCalculator = IsGraphingModeAvailable(); + if (supportGraphingCalculator) + { + const bool isEnabled = IsGraphingModeEnabled(); + res.push_back(NavCategoryInitializer{ ViewMode::Graphing, + GRAPHING_ID, + L"Graphing", + L"GraphingCalculatorMode", + L"\uF770", + CategoryGroupType::Calculator, + MyVirtualKey::Number3, + L"3", + SUPPORTS_ALL, + isEnabled }); + ++currentIndex; + } + res.insert( + res.end(), + { NavCategoryInitializer{ ViewMode::Programmer, + PROGRAMMER_ID, + L"Programmer", + L"ProgrammerMode", + L"\uECCE", + CategoryGroupType::Calculator, + supportGraphingCalculator ? MyVirtualKey::Number4 : MyVirtualKey::Number3, + towchar_t(currentIndex++), + SUPPORTS_ALL, + true }, + NavCategoryInitializer{ ViewMode::Date, + DATE_ID, + L"Date", + L"DateCalculationMode", + L"\uE787", + CategoryGroupType::Calculator, + supportGraphingCalculator ? MyVirtualKey::Number5 : MyVirtualKey::Number4, + towchar_t(currentIndex++), + SUPPORTS_ALL, + true }, + NavCategoryInitializer{ ViewMode::Currency, + CURRENCY_ID, + L"Currency", + L"CategoryName_Currency", + L"\uEB0D", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Volume, + VOLUME_ID, + L"Volume", + L"CategoryName_Volume", + L"\uF1AA", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Length, + LENGTH_ID, + L"Length", + L"CategoryName_Length", + L"\uECC6", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Weight, + WEIGHT_ID, + L"Weight and Mass", + L"CategoryName_Weight", + L"\uF4C1", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Temperature, + TEMPERATURE_ID, + L"Temperature", + L"CategoryName_Temperature", + L"\uE7A3", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + SUPPORTS_NEGATIVE, + true }, + NavCategoryInitializer{ ViewMode::Energy, + ENERGY_ID, + L"Energy", + L"CategoryName_Energy", + L"\uECAD", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Area, + AREA_ID, + L"Area", + L"CategoryName_Area", + L"\uE809", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Speed, + SPEED_ID, + L"Speed", + L"CategoryName_Speed", + L"\uEADA", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Time, + TIME_ID, + L"Time", + L"CategoryName_Time", + L"\uE917", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Power, + POWER_ID, + L"Power", + L"CategoryName_Power", + L"\uE945", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + SUPPORTS_NEGATIVE, + true }, + NavCategoryInitializer{ ViewMode::Data, + DATA_ID, + L"Data", + L"CategoryName_Data", + L"\uF20F", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Pressure, + PRESSURE_ID, + L"Pressure", + L"CategoryName_Pressure", + L"\uEC4A", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + POSITIVE_ONLY, + true }, + NavCategoryInitializer{ ViewMode::Angle, + ANGLE_ID, + L"Angle", + L"CategoryName_Angle", + L"\uF515", + CategoryGroupType::Converter, + MyVirtualKey::None, + nullptr, + SUPPORTS_NEGATIVE, + true } }); + return res; +}(); // This function should only be used when storing the mode to app data. int NavCategory::Serialize(ViewMode mode) @@ -211,6 +327,14 @@ ViewMode NavCategory::Deserialize(Platform::Object ^ obj) if (iter != s_categoryManifest.end()) { + if (iter->viewMode == ViewMode::Graphing) + { + // check if the user is allowed to use this feature + if (!IsGraphingModeEnabled()) + { + return ViewMode::None; + } + } return iter->viewMode; } } @@ -228,9 +352,13 @@ bool NavCategory::IsValidViewMode(ViewMode mode) bool NavCategory::IsCalculatorViewMode(ViewMode mode) { - // Historically, Date Calculator is not a Calculator mode - // even though it is in the Calculator category. - return !IsDateCalculatorViewMode(mode) && IsModeInCategoryGroup(mode, CategoryGroupType::Calculator); + // Historically, Calculator modes are Standard, Scientific, and Programmer. + return !IsDateCalculatorViewMode(mode) && !IsGraphingCalculatorViewMode(mode) && IsModeInCategoryGroup(mode, CategoryGroupType::Calculator); +} + +bool NavCategory::IsGraphingCalculatorViewMode(ViewMode mode) +{ + return mode == ViewMode::Graphing; } bool NavCategory::IsDateCalculatorViewMode(ViewMode mode) @@ -389,10 +517,12 @@ NavCategoryGroup::NavCategoryGroup(const NavCategoryGroupInitializer& groupIniti categoryName, categoryAutomationName, StringReference(categoryInitializer.glyph), - resProvider->GetResourceString(nameResourceKey + "AccessKey"), + categoryInitializer.accessKey != nullptr ? ref new String(categoryInitializer.accessKey) + : resProvider->GetResourceString(nameResourceKey + "AccessKey"), groupMode, categoryInitializer.viewMode, - categoryInitializer.supportsNegative)); + categoryInitializer.supportsNegative, + categoryInitializer.isEnabled)); } } } @@ -407,29 +537,12 @@ IObservableVector ^ NavCategoryGroup::CreateMenuOptions() NavCategoryGroup ^ NavCategoryGroup::CreateCalculatorCategory() { - return ref new NavCategoryGroup(s_categoryGroupManifest.at(0)); + return ref new NavCategoryGroup( + NavCategoryGroupInitializer{ CategoryGroupType::Calculator, L"CalculatorModeTextCaps", L"CalculatorModeText", L"CalculatorModePluralText" }); } NavCategoryGroup ^ NavCategoryGroup::CreateConverterCategory() { - return ref new NavCategoryGroup(s_categoryGroupManifest.at(1)); -} - -vector NavCategoryGroup::GetInitializerCategoryGroup(CategoryGroupType groupType) -{ - vector initializers{}; - copy_if(begin(s_categoryManifest), end(s_categoryManifest), back_inserter(initializers), [groupType](const NavCategoryInitializer& initializer) { - return initializer.groupType == groupType; - }); - - return initializers; -} - -String ^ NavCategoryGroup::GetHeaderResourceKey(CategoryGroupType type) -{ - auto iter = find_if(begin(s_categoryGroupManifest), end(s_categoryGroupManifest), [type](const NavCategoryGroupInitializer& initializer) { - return initializer.type == type; - }); - - return (iter != s_categoryGroupManifest.end()) ? StringReference(iter->headerResourceKey) : nullptr; + return ref new NavCategoryGroup( + NavCategoryGroupInitializer{ CategoryGroupType::Converter, L"ConverterModeTextCaps", L"ConverterModeText", L"ConverterModePluralText" }); } diff --git a/src/CalcViewModel/Common/NavCategory.h b/src/CalcViewModel/Common/NavCategory.h index b1cf1d3d3..a2edc9954 100644 --- a/src/CalcViewModel/Common/NavCategory.h +++ b/src/CalcViewModel/Common/NavCategory.h @@ -44,7 +44,8 @@ namespace CalculatorApp Data = 13, Pressure = 14, Angle = 15, - Currency = 16 + Currency = 16, + Graphing = 17 }; public @@ -66,7 +67,9 @@ namespace CalculatorApp wchar_t const* glyph, CategoryGroupType group, MyVirtualKey vKey, - bool categorySupportsNegative) + wchar_t const* aKey, + bool categorySupportsNegative, + bool enabled) : viewMode(mode) , serializationId(id) , friendlyName(name) @@ -74,7 +77,9 @@ namespace CalculatorApp , glyph(glyph) , groupType(group) , virtualKey(vKey) + , accessKey(aKey) , supportsNegative(categorySupportsNegative) + , isEnabled(enabled) { } @@ -85,7 +90,9 @@ namespace CalculatorApp const wchar_t* const glyph; const CategoryGroupType groupType; const MyVirtualKey virtualKey; + const wchar_t* const accessKey; const bool supportsNegative; + const bool isEnabled; }; private @@ -109,45 +116,17 @@ namespace CalculatorApp { public: OBSERVABLE_OBJECT(); + PROPERTY_R(Platform::String ^, Name); + PROPERTY_R(Platform::String ^, AutomationName); + PROPERTY_R(Platform::String ^, Glyph); + PROPERTY_R(ViewMode, Mode); + PROPERTY_R(Platform::String ^, AccessKey); + PROPERTY_R(bool, SupportsNegative); + PROPERTY_R(bool, IsEnabled); property Platform::String - ^ Name { Platform::String ^ get() { return m_name; } } + ^ AutomationId { Platform::String ^ get() { return m_Mode.ToString(); } } - property Platform::String - ^ AutomationName { Platform::String ^ get() { return m_automationName; } } - - property Platform::String - ^ Glyph { Platform::String ^ get() { return m_glyph; } } - - property int Position - { - int get() - { - return m_position; - } - } - - property ViewMode Mode - { - ViewMode get() - { - return m_viewMode; - } - } - - property Platform::String - ^ AutomationId { Platform::String ^ get() { return m_viewMode.ToString(); } } - - property Platform::String - ^ AccessKey { Platform::String ^ get() { return m_accessKey; } } - - property bool SupportsNegative - { - bool get() - { - return m_supportsNegative; - } - } // For saving/restoring last mode used. static int Serialize(ViewMode mode); @@ -156,6 +135,7 @@ namespace CalculatorApp static bool IsValidViewMode(ViewMode mode); static bool IsCalculatorViewMode(ViewMode mode); + static bool IsGraphingCalculatorViewMode(ViewMode mode); static bool IsDateCalculatorViewMode(ViewMode mode); static bool IsConverterViewMode(ViewMode mode); @@ -178,16 +158,17 @@ namespace CalculatorApp Platform::String ^ accessKey, Platform::String ^ mode, ViewMode viewMode, - bool supportsNegative) - : m_name(name) - , m_automationName(automationName) - , m_glyph(glyph) - , m_accessKey(accessKey) - , m_mode(mode) - , m_viewMode(viewMode) - , m_supportsNegative(supportsNegative) + bool supportsNegative, + bool isEnabled) + : m_Name(name) + , m_AutomationName(automationName) + , m_Glyph(glyph) + , m_AccessKey(accessKey) + , m_modeString(mode) + , m_Mode(viewMode) + , m_SupportsNegative(supportsNegative) + , m_IsEnabled(isEnabled) { - m_position = NavCategory::GetPosition(m_viewMode); } static std::vector GetCategoryAcceleratorKeys(); @@ -195,14 +176,7 @@ namespace CalculatorApp private: static bool IsModeInCategoryGroup(ViewMode mode, CategoryGroupType groupType); - ViewMode m_viewMode; - Platform::String ^ m_name; - Platform::String ^ m_automationName; - Platform::String ^ m_glyph; - Platform::String ^ m_accessKey; - Platform::String ^ m_mode; - int m_position; - bool m_supportsNegative; + Platform::String ^ m_modeString; }; [Windows::UI::Xaml::Data::Bindable] public ref class NavCategoryGroup sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged @@ -216,15 +190,11 @@ namespace CalculatorApp static Windows::Foundation::Collections::IObservableVector ^ CreateMenuOptions(); - static Platform::String ^ GetHeaderResourceKey(CategoryGroupType type); - internal : static NavCategoryGroup ^ CreateCalculatorCategory(); static NavCategoryGroup ^ CreateConverterCategory(); private: NavCategoryGroup(const NavCategoryGroupInitializer& groupInitializer); - - static std::vector GetInitializerCategoryGroup(CategoryGroupType groupType); }; } } diff --git a/src/CalcViewModel/Common/Utils.cpp b/src/CalcViewModel/Common/Utils.cpp index 368aa472d..27c325415 100644 --- a/src/CalcViewModel/Common/Utils.cpp +++ b/src/CalcViewModel/Common/Utils.cpp @@ -14,11 +14,13 @@ using namespace CalculatorApp; using namespace CalculatorApp::Common; using namespace concurrency; +using namespace Graphing::Renderer; using namespace Platform; using namespace std; using namespace Utils; using namespace Windows::ApplicationModel::Resources; using namespace Windows::Storage::Streams; +using namespace Windows::UI; using namespace Windows::UI::Core; using namespace Windows::UI::ViewManagement; using namespace Windows::UI::Xaml; @@ -196,3 +198,118 @@ task Utils::ReadFileFromFolder(IStorageFolder ^ folder, String ^ fileN String ^ contents = co_await FileIO::ReadTextAsync(file); co_return contents; } + +bool Utils::AreColorsEqual(const Color& color1, const Color& color2) +{ + return ((color1.A == color2.A) + && (color1.R == color2.R) + && (color1.G == color2.G) + && (color1.B == color2.B)); +} + +String^ Utils::Trim(String^ value) +{ + if (!value) + { + return nullptr; + } + + wstring trimmed = value->Data(); + Trim(trimmed); + return ref new String(trimmed.c_str()); +} + +void Utils::Trim(wstring& value) +{ + TrimFront(value); + TrimBack(value); +} + +void Utils::TrimFront(wstring& value) +{ + value.erase(value.begin(), find_if(value.cbegin(), value.cend(), [](int ch){ + return !isspace(ch); + })); +} + +void Utils::TrimBack(wstring& value) +{ + value.erase(find_if(value.crbegin(), value.crend(), [](int ch) { + return !isspace(ch); + }).base(), value.end()); +} + +String^ Utils::EscapeHtmlSpecialCharacters(String^ originalString, shared_ptr> specialCharacters) +{ + // Construct a default special characters if not provided. + if (specialCharacters == nullptr) + { + specialCharacters = make_shared>(); + specialCharacters->push_back(L'&'); + specialCharacters->push_back(L'\"'); + specialCharacters->push_back(L'\''); + specialCharacters->push_back(L'<'); + specialCharacters->push_back(L'>'); + } + + bool replaceCharacters = false; + const wchar_t* pCh; + String^ replacementString = nullptr; + + // First step is scanning the string for special characters. + // If there isn't any special character, we simply return the original string + for (pCh = originalString->Data(); *pCh; pCh++) + { + if (std::find(specialCharacters->begin(), specialCharacters->end(), *pCh) != specialCharacters->end()) + { + replaceCharacters = true; + break; + } + } + + if (replaceCharacters) + { + // If we indeed find a special character, we step back one character (the special + // character), and we create a new string where we replace those characters one by one + pCh--; + wstringstream buffer; + buffer << wstring(originalString->Data(), pCh); + + for (; *pCh; pCh++) + { + switch (*pCh) + { + case L'&': + buffer << L"&"; + break; + case L'\"': + buffer << L"""; + break; + case L'\'': + buffer << L"'"; + break; + case L'<': + buffer << L"<"; + break; + case L'>': + buffer << L">"; + break; + default: + buffer << *pCh; + } + } + replacementString = ref new String(buffer.str().c_str()); + } + + return replaceCharacters ? replacementString : originalString; +} + +bool operator==(const Color& color1, const Color& color2) +{ + return equal_to()(color1, color2); +} + +bool operator!=(const Color& color1, const Color& color2) +{ + return !(color1 == color2); +} diff --git a/src/CalcViewModel/Common/Utils.h b/src/CalcViewModel/Common/Utils.h index 2c7523827..9e532870e 100644 --- a/src/CalcViewModel/Common/Utils.h +++ b/src/CalcViewModel/Common/Utils.h @@ -5,9 +5,13 @@ #include "CalcManager/ExpressionCommandInterface.h" #include "DelegateCommand.h" +#include "GraphingInterfaces/GraphingEnums.h" // Utility macros to make Models easier to write // generates a member variable called m_ + +#define SINGLE_ARG(...) __VA_ARGS__ + #define PROPERTY_R(t, n) \ property t n \ { \ @@ -405,12 +409,18 @@ namespace Utils Windows::Foundation::DateTime GetUniversalSystemTime(); bool IsDateTimeOlderThan(Windows::Foundation::DateTime dateTime, const long long duration); - concurrency::task WriteFileToFolder( - Windows::Storage::IStorageFolder ^ folder, - Platform::String ^ fileName, - Platform::String ^ contents, - Windows::Storage::CreationCollisionOption collisionOption); - concurrency::task ReadFileFromFolder(Windows::Storage::IStorageFolder ^ folder, Platform::String ^ fileName); + concurrency::task WriteFileToFolder(Windows::Storage::IStorageFolder^ folder, Platform::String^ fileName, Platform::String^ contents, Windows::Storage::CreationCollisionOption collisionOption); + concurrency::task ReadFileFromFolder(Windows::Storage::IStorageFolder^ folder, Platform::String^ fileName); + + bool AreColorsEqual(const Windows::UI::Color& color1, const Windows::UI::Color& color2); + + Platform::String^ Trim(Platform::String^ value); + void Trim(std::wstring& value); + void TrimFront(std::wstring& value); + void TrimBack(std::wstring& value); + + Platform::String ^ EscapeHtmlSpecialCharacters(Platform::String ^ originalString, std::shared_ptr> specialCharacters = nullptr); + } // This goes into the header to define the property, in the public: section of the class @@ -707,3 +717,18 @@ namespace CalculatorApp return to; } } + +// There's no standard definition of equality for Windows::UI::Color structs. +// Define a template specialization for std::equal_to. +template<> +class std::equal_to +{ +public: + bool operator()(const Windows::UI::Color& color1, const Windows::UI::Color& color2) + { + return Utils::AreColorsEqual(color1, color2); + } +}; + +bool operator==(const Windows::UI::Color& color1, const Windows::UI::Color& color2); +bool operator!=(const Windows::UI::Color& color1, const Windows::UI::Color& color2); diff --git a/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h b/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h index d2548dcf1..f4b5082d0 100644 --- a/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h +++ b/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #pragma once diff --git a/src/CalcViewModel/GraphingCalculator/EquationViewModel.cpp b/src/CalcViewModel/GraphingCalculator/EquationViewModel.cpp new file mode 100644 index 000000000..94b97794f --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/EquationViewModel.cpp @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "EquationViewModel.h" +#include "CalcViewModel\Common\LocalizationSettings.h" +#include "CalcViewModel\GraphingCalculatorEnums.h" + +using namespace CalculatorApp::Common; +using namespace Graphing; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::Foundation::Collections; +using namespace GraphControl; + +namespace CalculatorApp::ViewModel +{ + GridDisplayItems::GridDisplayItems() + : m_Expression{ "" } + , m_Direction{ "" } + { + } + + KeyGraphFeaturesItem::KeyGraphFeaturesItem() + : m_Title{ "" } + , m_DisplayItems{ ref new Vector() } + , m_GridItems{ ref new Vector() } + , m_IsText{ false } + { + } + + EquationViewModel::EquationViewModel(Equation ^ equation, int functionLabelIndex, Windows::UI::Color color) + : m_AnalysisErrorVisible{ false } + , m_FunctionLabelIndex{ functionLabelIndex } + , m_KeyGraphFeaturesItems{ ref new Vector() } + , m_resourceLoader{ Windows::ApplicationModel::Resources::ResourceLoader::GetForCurrentView() } + { + if (equation == nullptr) + { + throw ref new InvalidArgumentException(L"Equation cannot be null"); + } + + GraphEquation = equation; + LineColor = color; + IsLineEnabled = true; + } + + void EquationViewModel::PopulateKeyGraphFeatures(KeyGraphFeaturesInfo ^ graphEquation) + { + if (graphEquation->AnalysisError != 0) + { + AnalysisErrorVisible = true; + if (graphEquation->AnalysisError == static_cast(AnalysisErrorType::AnalysisCouldNotBePerformed)) + { + AnalysisErrorString = m_resourceLoader->GetString(L"KGFAnalysisCouldNotBePerformed"); + } + else if (graphEquation->AnalysisError == static_cast(AnalysisErrorType::AnalysisNotSupported)) + { + AnalysisErrorString = m_resourceLoader->GetString(L"KGFAnalysisNotSupported"); + } + return; + } + + KeyGraphFeaturesItems->Clear(); + + AddKeyGraphFeature(m_resourceLoader->GetString(L"Domain"), graphEquation->Domain, m_resourceLoader->GetString(L"KGFDomainNone")); + AddKeyGraphFeature(m_resourceLoader->GetString(L"Range"), graphEquation->Range, m_resourceLoader->GetString(L"KGFRangeNone")); + AddKeyGraphFeature(m_resourceLoader->GetString(L"XIntercept"), graphEquation->XIntercept, m_resourceLoader->GetString(L"KGFXInterceptNone")); + AddKeyGraphFeature(m_resourceLoader->GetString(L"YIntercept"), graphEquation->YIntercept, m_resourceLoader->GetString(L"KGFYInterceptNone")); + AddKeyGraphFeature(m_resourceLoader->GetString(L"Minima"), graphEquation->Minima, m_resourceLoader->GetString(L"KGFMinimaNone")); + AddKeyGraphFeature(m_resourceLoader->GetString(L"Maxima"), graphEquation->Maxima, m_resourceLoader->GetString(L"KGFMaximaNone")); + AddKeyGraphFeature(m_resourceLoader->GetString(L"InflectionPoints"), graphEquation->InflectionPoints, m_resourceLoader->GetString(L"KGFInflectionPointsNone")); + AddKeyGraphFeature( + m_resourceLoader->GetString(L"VerticalAsymptotes"), graphEquation->VerticalAsymptotes, m_resourceLoader->GetString(L"KGFVerticalAsymptotesNone")); + AddKeyGraphFeature( + m_resourceLoader->GetString(L"HorizontalAsymptotes"), graphEquation->HorizontalAsymptotes, m_resourceLoader->GetString(L"KGFHorizontalAsymptotesNone")); + AddKeyGraphFeature( + m_resourceLoader->GetString(L"ObliqueAsymptotes"), graphEquation->ObliqueAsymptotes, m_resourceLoader->GetString(L"KGFObliqueAsymptotesNone")); + AddParityKeyGraphFeature(graphEquation); + AddPeriodicityKeyGraphFeature(graphEquation); + AddMonotoncityKeyGraphFeature(graphEquation); + AddTooComplexKeyGraphFeature(graphEquation); + + AnalysisErrorVisible = false; + } + + void EquationViewModel::AddKeyGraphFeature(String ^ title, String ^ expression, String ^ errorString) + { + KeyGraphFeaturesItem ^ item = ref new KeyGraphFeaturesItem(); + item->Title = title; + if (expression != L"") + { + item->DisplayItems->Append(expression); + item->IsText = false; + } + else + { + item->DisplayItems->Append(errorString); + item->IsText = true; + } + KeyGraphFeaturesItems->Append(item); + } + + void EquationViewModel::AddKeyGraphFeature(String ^ title, IVector ^ expressionVector, String ^ errorString) + { + KeyGraphFeaturesItem ^ item = ref new KeyGraphFeaturesItem(); + item->Title = title; + if (expressionVector->Size != 0) + { + for (auto expression : expressionVector) + { + item->DisplayItems->Append(expression); + } + item->IsText = false; + } + else + { + item->DisplayItems->Append(errorString); + item->IsText = true; + } + KeyGraphFeaturesItems->Append(item); + } + + void EquationViewModel::AddParityKeyGraphFeature(KeyGraphFeaturesInfo ^ graphEquation) + { + KeyGraphFeaturesItem ^ parityItem = ref new KeyGraphFeaturesItem(); + parityItem->Title = m_resourceLoader->GetString(L"Parity"); + switch (graphEquation->Parity) + { + case 0: + parityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFParityUnknown")); + break; + case 1: + parityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFParityOdd")); + break; + case 2: + parityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFParityEven")); + break; + case 3: + parityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFParityNeither")); + break; + default: + parityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFParityUnknown")); + } + parityItem->IsText = true; + + KeyGraphFeaturesItems->Append(parityItem); + } + + void EquationViewModel::AddPeriodicityKeyGraphFeature(KeyGraphFeaturesInfo ^ graphEquation) + { + KeyGraphFeaturesItem ^ periodicityItem = ref new KeyGraphFeaturesItem(); + periodicityItem->Title = m_resourceLoader->GetString(L"Periodicity"); + switch (graphEquation->PeriodicityDirection) + { + case 0: + // Periodicity is not supported or is too complex to calculate. + // Return out of this function without adding periodicity to KeyGraphFeatureItems. + // SetTooComplexFeaturesErrorProperty will set the too complex error when periodicity is supported and unknown + return; + case 1: + if (graphEquation->PeriodicityExpression == L"") + { + periodicityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFPeriodicityUnknown")); + periodicityItem->IsText = true; + } + else + { + periodicityItem->DisplayItems->Append(graphEquation->PeriodicityExpression); + periodicityItem->IsText = false; + } + break; + case 2: + periodicityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFPeriodicityNotPeriodic")); + periodicityItem->IsText = false; + break; + default: + periodicityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFPeriodicityError")); + periodicityItem->IsText = true; + } + + KeyGraphFeaturesItems->Append(periodicityItem); + } + + void EquationViewModel::AddMonotoncityKeyGraphFeature(KeyGraphFeaturesInfo ^ graphEquation) + { + KeyGraphFeaturesItem ^ monotonicityItem = ref new KeyGraphFeaturesItem(); + monotonicityItem->Title = m_resourceLoader->GetString(L"Monotonicity"); + if (graphEquation->Monotonicity->Size != 0) + { + for (auto item : graphEquation->Monotonicity) + { + GridDisplayItems ^ gridItem = ref new GridDisplayItems(); + gridItem->Expression = item->Key; + + auto monotonicityType = item->Value->Data(); + switch (*monotonicityType) + { + case '0': + gridItem->Direction = m_resourceLoader->GetString(L"KGFMonotonicityUnknown"); + break; + case '1': + gridItem->Direction = m_resourceLoader->GetString(L"KGFMonotonicityIncreasing"); + break; + case '2': + gridItem->Direction = m_resourceLoader->GetString(L"KGFMonotonicityDecreasing"); + break; + case '3': + gridItem->Direction = m_resourceLoader->GetString(L"KGFMonotonicityConstant"); + break; + default: + gridItem->Direction = m_resourceLoader->GetString(L"KGFMonotonicityError"); + break; + } + + monotonicityItem->GridItems->Append(gridItem); + } + monotonicityItem->IsText = false; + } + else + { + monotonicityItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFMonotonicityError")); + monotonicityItem->IsText = true; + } + + KeyGraphFeaturesItems->Append(monotonicityItem); + } + + void EquationViewModel::AddTooComplexKeyGraphFeature(KeyGraphFeaturesInfo ^ graphEquation) + { + if (graphEquation->TooComplexFeatures <= 0) + { + return; + } + + Platform::String ^ separator = ref new String(LocalizationSettings::GetInstance().GetListSeparator().c_str()); + + wstring error; + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Domain) == KeyGraphFeaturesFlag::Domain) + { + error.append((m_resourceLoader->GetString(L"Domain") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Range) == KeyGraphFeaturesFlag::Range) + { + error.append((m_resourceLoader->GetString(L"Range") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Zeros) == KeyGraphFeaturesFlag::Zeros) + { + error.append((m_resourceLoader->GetString(L"XIntercept") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::YIntercept) == KeyGraphFeaturesFlag::YIntercept) + { + error.append((m_resourceLoader->GetString(L"YIntercept") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Parity) == KeyGraphFeaturesFlag::Parity) + { + error.append((m_resourceLoader->GetString(L"Parity") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Periodicity) == KeyGraphFeaturesFlag::Periodicity) + { + error.append((m_resourceLoader->GetString(L"Periodicity") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Minima) == KeyGraphFeaturesFlag::Minima) + { + error.append((m_resourceLoader->GetString(L"Minima") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::Maxima) == KeyGraphFeaturesFlag::Maxima) + { + error.append((m_resourceLoader->GetString(L"Maxima") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::InflectionPoints) == KeyGraphFeaturesFlag::InflectionPoints) + { + error.append((m_resourceLoader->GetString(L"InflectionPoints") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::VerticalAsymptotes) == KeyGraphFeaturesFlag::VerticalAsymptotes) + { + error.append((m_resourceLoader->GetString(L"VerticalAsymptotes") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::HorizontalAsymptotes) == KeyGraphFeaturesFlag::HorizontalAsymptotes) + { + error.append((m_resourceLoader->GetString(L"HorizontalAsymptotes") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::ObliqueAsymptotes) == KeyGraphFeaturesFlag::ObliqueAsymptotes) + { + error.append((m_resourceLoader->GetString(L"ObliqueAsymptotes") + separator + L" ")->Data()); + } + if ((graphEquation->TooComplexFeatures & KeyGraphFeaturesFlag::MonotoneIntervals) == KeyGraphFeaturesFlag::MonotoneIntervals) + { + error.append((m_resourceLoader->GetString(L"Monotonicity") + separator + L" ")->Data()); + } + + KeyGraphFeaturesItem ^ tooComplexItem = ref new KeyGraphFeaturesItem(); + tooComplexItem->DisplayItems->Append(m_resourceLoader->GetString(L"KGFTooComplexFeaturesError")); + tooComplexItem->DisplayItems->Append(ref new String(error.substr(0, (error.length() - (separator->Length() + 1))).c_str())); + tooComplexItem->IsText = true; + + KeyGraphFeaturesItems->Append(tooComplexItem); + } +} diff --git a/src/CalcViewModel/GraphingCalculator/EquationViewModel.h b/src/CalcViewModel/GraphingCalculator/EquationViewModel.h new file mode 100644 index 000000000..5ed0870da --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/EquationViewModel.h @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "../Common/Utils.h" + +namespace GraphControl +{ + ref class Equation; + ref class KeyGraphFeaturesInfo; +} + +namespace CalculatorApp::ViewModel +{ +public + ref class GridDisplayItems sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + GridDisplayItems(); + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(Platform::String ^, Expression); + OBSERVABLE_PROPERTY_RW(Platform::String ^, Direction); + }; + +public + ref class KeyGraphFeaturesItem sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + KeyGraphFeaturesItem(); + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(Platform::String ^, Title); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IObservableVector ^, DisplayItems); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IObservableVector ^, GridItems); + OBSERVABLE_PROPERTY_RW(bool, IsText); + }; + +public + ref class EquationViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + EquationViewModel(GraphControl::Equation ^ equation, int functionLabelIndex, Windows::UI::Color color); + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_R(GraphControl::Equation ^, GraphEquation); + OBSERVABLE_PROPERTY_R(int, FunctionLabelIndex); + OBSERVABLE_PROPERTY_RW(bool, IsLastItemInList); + + property Platform::String ^ Expression + { + Platform::String ^ get() + { + return GraphEquation->Expression; + } + void set(Platform::String ^ value) + { + if (GraphEquation->Expression != value) + { + GraphEquation->Expression = value; + RaisePropertyChanged("Expression"); + } + } + } + + property Windows::UI::Color LineColor + { + Windows::UI::Color get() + { + return GraphEquation->LineColor; + } + void set(Windows::UI::Color value) + { + if (!Utils::AreColorsEqual(GraphEquation->LineColor, value)) + { + GraphEquation->LineColor = value; + RaisePropertyChanged("LineColor"); + } + } + } + + property bool IsLineEnabled + { + bool get() + { + return GraphEquation->IsLineEnabled; + } + void set(bool value) + { + if (GraphEquation->IsLineEnabled != value) + { + GraphEquation->IsLineEnabled = value; + RaisePropertyChanged("IsLineEnabled"); + } + } + } + + // Key Graph Features + OBSERVABLE_PROPERTY_R(Platform::String ^, AnalysisErrorString); + OBSERVABLE_PROPERTY_R(bool, AnalysisErrorVisible); + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector ^, KeyGraphFeaturesItems) + + void PopulateKeyGraphFeatures(GraphControl::KeyGraphFeaturesInfo ^ info); + + private: + void AddKeyGraphFeature(Platform::String ^ title, Platform::String ^ expression, Platform::String ^ errorString); + void AddKeyGraphFeature( + Platform::String ^ title, + Windows::Foundation::Collections::IVector ^ expressionVector, + Platform::String ^ errorString); + void AddParityKeyGraphFeature(GraphControl::KeyGraphFeaturesInfo ^ info); + void AddPeriodicityKeyGraphFeature(GraphControl::KeyGraphFeaturesInfo ^ info); + void AddMonotoncityKeyGraphFeature(GraphControl::KeyGraphFeaturesInfo ^ info); + void AddTooComplexKeyGraphFeature(GraphControl::KeyGraphFeaturesInfo ^ info); + + Windows::Foundation::Collections::IObservableMap ^ m_Monotonicity; + Windows::ApplicationModel::Resources::ResourceLoader ^ m_resourceLoader; + }; +} diff --git a/src/CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.cpp b/src/CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.cpp new file mode 100644 index 000000000..974cf8f85 --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "GraphingCalculatorViewModel.h" + +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace Platform::Collections; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml::Data; + +namespace CalculatorApp::ViewModel +{ + GraphingCalculatorViewModel::GraphingCalculatorViewModel() + : m_IsDecimalEnabled{ true } + , m_Equations{ ref new Vector() } + , m_Variables{ ref new Vector() } + { + } + + void GraphingCalculatorViewModel::OnButtonPressed(Object ^ parameter) + { + } + + void GraphingCalculatorViewModel::UpdateVariables(IMap ^ variables) + { + Variables->Clear(); + for (auto var : variables) + { + auto variable = ref new VariableViewModel(var->Key, var->Value); + variable->VariableUpdated += ref new EventHandler([this, variable](Object ^ sender, VariableChangedEventArgs e) { + VariableUpdated(variable, VariableChangedEventArgs{ e.variableName, e.newValue }); + }); + Variables->Append(variable); + } + } + + void GraphingCalculatorViewModel::SetSelectedEquation(EquationViewModel ^ equation) + { + SelectedEquation = equation; + } +} diff --git a/src/CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.h b/src/CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.h new file mode 100644 index 000000000..78e89479b --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "../Common/Utils.h" +#include "EquationViewModel.h" +#include "VariableViewModel.h" + +namespace CalculatorApp::ViewModel +{ + [Windows::UI::Xaml::Data::Bindable] public ref class GraphingCalculatorViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + GraphingCalculatorViewModel(); + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_R(bool, IsDecimalEnabled); + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector ^, Equations); + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector ^, Variables); + OBSERVABLE_PROPERTY_R(EquationViewModel ^, SelectedEquation); + + COMMAND_FOR_METHOD(ButtonPressed, GraphingCalculatorViewModel::OnButtonPressed); + + event Windows::Foundation::EventHandler ^ VariableUpdated; + + void UpdateVariables(Windows::Foundation::Collections::IMap ^ variables); + + void SetSelectedEquation(EquationViewModel ^ equation); + private: + void OnButtonPressed(Platform::Object ^ parameter); + }; +} diff --git a/src/CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.cpp b/src/CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.cpp new file mode 100644 index 000000000..c261890cd --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.cpp @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "GraphingSettingsViewModel.h" +#include + +using namespace CalculatorApp::ViewModel; +using namespace CalcManager::NumberFormattingUtils; +using namespace GraphControl; +using namespace std; +using namespace Platform; + +GraphingSettingsViewModel::GraphingSettingsViewModel() + : m_XMinValue(0) + , m_XMaxValue(0) + , m_YMinValue(0) + , m_YMaxValue(0) + , m_XMinError(false) + , m_XMaxError(false) + , m_YMinError(false) + , m_YMaxError(false) + , m_dontUpdateDisplayRange(false) + , m_XIsMinLastChanged(true) + , m_YIsMinLastChanged(true) +{ +} + +void GraphingSettingsViewModel::SetGrapher(Grapher ^ grapher) +{ + if (grapher != nullptr) + { + if (grapher->TrigUnitMode == (int)Graphing::EvalTrigUnitMode::Invalid) + { + grapher->TrigUnitMode = (int)Graphing::EvalTrigUnitMode::Radians; + } + } + Graph = grapher; + InitRanges(); + RaisePropertyChanged(L"TrigUnit"); +} + +void GraphingSettingsViewModel::InitRanges() +{ + double xMin = 0, xMax = 0, yMin = 0, yMax = 0; + if (m_Graph != nullptr) + { + m_Graph->GetDisplayRanges(&xMin, &xMax, &yMin, &yMax); + } + m_dontUpdateDisplayRange = true; + m_XMinValue = xMin; + m_XMaxValue = xMax; + m_YMinValue = yMin; + m_YMaxValue = yMax; + auto valueStr = to_wstring(m_XMinValue); + TrimTrailingZeros(valueStr); + m_XMin = ref new String(valueStr.c_str()); + + valueStr = to_wstring(m_XMaxValue); + TrimTrailingZeros(valueStr); + m_XMax = ref new String(valueStr.c_str()); + + valueStr = to_wstring(m_YMinValue); + TrimTrailingZeros(valueStr); + m_YMin = ref new String(valueStr.c_str()); + + valueStr = to_wstring(m_YMaxValue); + TrimTrailingZeros(valueStr); + m_YMax = ref new String(valueStr.c_str()); + + m_dontUpdateDisplayRange = false; +} + +void GraphingSettingsViewModel::UpdateDisplayRange(bool XValuesModified) +{ + if (m_Graph == nullptr || m_dontUpdateDisplayRange || HasError()) + { + return; + } + + if (m_Graph->ForceProportionalAxes) + { + // If ForceProportionalAxes is set, the graph will try to automatically adjust ranges to remain proportional. + // but without a logic to choose which values can be modified or not. + // To solve this problem, we calculate the new ranges here, taking care to not modify the current axis and + // modifying only the least recently updated value of the other axis. + + if (XValuesModified) + { + if (m_YIsMinLastChanged) + { + auto yMaxValue = m_YMinValue + (m_XMaxValue - m_XMinValue) * m_Graph->ActualHeight / m_Graph->ActualWidth; + if (m_YMaxValue != yMaxValue) + { + m_YMaxValue = yMaxValue; + auto valueStr = to_wstring(m_YMaxValue); + TrimTrailingZeros(valueStr); + m_YMax = ref new String(valueStr.c_str()); + RaisePropertyChanged("YMax"); + } + } + else + { + auto yMinValue = m_YMaxValue - (m_XMaxValue - m_XMinValue) * m_Graph->ActualHeight / m_Graph->ActualWidth; + if (m_YMinValue != yMinValue) + { + m_YMinValue = yMinValue; + auto valueStr = to_wstring(m_YMinValue); + TrimTrailingZeros(valueStr); + m_YMin = ref new String(valueStr.c_str()); + RaisePropertyChanged("YMin"); + } + } + } + else + { + if (m_XIsMinLastChanged) + { + auto xMaxValue = m_XMinValue + (m_YMaxValue - m_YMinValue) * m_Graph->ActualWidth / m_Graph->ActualHeight; + if (m_XMaxValue != xMaxValue) + { + m_XMaxValue = xMaxValue; + auto valueStr = to_wstring(m_XMaxValue); + TrimTrailingZeros(valueStr); + m_XMax = ref new String(valueStr.c_str()); + RaisePropertyChanged("XMax"); + } + } + else + { + auto xMinValue = m_XMaxValue - (m_YMaxValue - m_YMinValue) * m_Graph->ActualWidth / m_Graph->ActualHeight; + if (m_XMinValue != xMinValue) + { + m_XMinValue = xMinValue; + auto valueStr = to_wstring(m_XMinValue); + TrimTrailingZeros(valueStr); + m_XMin = ref new String(valueStr.c_str()); + RaisePropertyChanged("XMin"); + } + } + } + } + m_Graph->SetDisplayRanges(m_XMinValue, m_XMaxValue, m_YMinValue, m_YMaxValue); +} + +bool GraphingSettingsViewModel::HasError() +{ + return m_XMinError || m_YMinError || m_XMaxError || m_YMaxError || XError || YError; +} diff --git a/src/CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.h b/src/CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.h new file mode 100644 index 000000000..ed250bebd --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.h @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "../Common/Utils.h" + +namespace CalculatorApp::ViewModel +{ +#pragma once + [Windows::UI::Xaml::Data::Bindable] public ref class GraphingSettingsViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_R(bool, YMinError); + OBSERVABLE_PROPERTY_R(bool, XMinError); + OBSERVABLE_PROPERTY_R(bool, XMaxError); + OBSERVABLE_PROPERTY_R(bool, YMaxError); + OBSERVABLE_PROPERTY_R(GraphControl::Grapher ^, Graph); + + GraphingSettingsViewModel(); + + property bool XError + { + bool get() + { + return !m_XMinError && !m_XMaxError && m_XMinValue >= m_XMaxValue; + } + } + + property bool YError + { + bool get() + { + return !m_YMinError && !m_YMaxError && m_YMinValue >= m_YMaxValue; + } + } + + property Platform::String ^ XMin + { + Platform::String ^ get() + { + return m_XMin; + } + void set(Platform::String ^ value) + { + if (m_XMin == value) + { + return; + } + m_XMin = value; + m_XIsMinLastChanged = true; + if (m_Graph != nullptr) + { + try + { + size_t sz; + auto number = std::stod(value->Data(), &sz); + if (value->Length() == sz) + { + m_Graph->XAxisMin = m_XMinValue = number; + XMinError = false; + } + else + { + XMinError = true; + } + } + catch (...) + { + XMinError = true; + } + } + RaisePropertyChanged("XError"); + RaisePropertyChanged("XMin"); + UpdateDisplayRange(true); + } + } + + property Platform::String ^ XMax + { + Platform::String ^ get() + { + return m_XMax; + } + void set(Platform::String ^ value) + { + if (m_XMax == value) + { + return; + } + m_XMax = value; + m_XIsMinLastChanged = false; + if (m_Graph != nullptr) + { + try + { + size_t sz; + auto number = std::stod(value->Data(), &sz); + if (value->Length() == sz) + { + m_Graph->XAxisMax = m_XMaxValue = number; + XMaxError = false; + } + else + { + XMaxError = true; + } + } + catch (...) + { + XMaxError = true; + } + } + RaisePropertyChanged("XError"); + RaisePropertyChanged("XMax"); + UpdateDisplayRange(true); + } + } + + property Platform::String ^ YMin + { + Platform::String ^ get() + { + return m_YMin; + } + void set(Platform::String ^ value) + { + if (m_YMin == value) + { + return; + } + m_YMin = value; + m_YIsMinLastChanged = true; + if (m_Graph != nullptr) + { + try + { + size_t sz; + auto number = std::stod(value->Data(), &sz); + if (value->Length() == sz) + { + m_Graph->YAxisMin = m_YMinValue = number; + YMinError = false; + } + else + { + YMinError = true; + } + } + catch (...) + { + YMinError = true; + } + } + RaisePropertyChanged("YError"); + RaisePropertyChanged("YMin"); + UpdateDisplayRange(false); + } + } + + property Platform::String ^ YMax + { + Platform::String ^ get() + { + return m_YMax; + } + void set(Platform::String ^ value) + { + if (m_YMax == value) + { + return; + } + m_YMax = value; + m_YIsMinLastChanged = false; + if (m_Graph != nullptr) + { + try + { + size_t sz; + auto number = std::stod(value->Data(), &sz); + if (value->Length() == sz) + { + m_Graph->YAxisMax = m_YMaxValue = number; + YMaxError = false; + } + else + { + YMaxError = true; + } + } + catch (...) + { + YMaxError = true; + } + } + RaisePropertyChanged("YError"); + RaisePropertyChanged("YMax"); + UpdateDisplayRange(false); + } + } + + property int TrigUnit + { + int get() + { + return m_Graph == nullptr ? (int)Graphing::EvalTrigUnitMode::Invalid : m_Graph->TrigUnitMode; + } + void set(int value) + { + if (m_Graph == nullptr) + { + return; + } + m_Graph->TrigUnitMode = value; + RaisePropertyChanged(L"TrigUnit"); + } + } + + property bool TrigModeRadians + { + bool get() + { + return m_Graph != nullptr && m_Graph->TrigUnitMode == (int)Graphing::EvalTrigUnitMode::Radians; + } + void set(bool value) + { + if (value && m_Graph != nullptr && m_Graph->TrigUnitMode != (int)Graphing::EvalTrigUnitMode::Radians) + { + m_Graph->TrigUnitMode = (int)Graphing::EvalTrigUnitMode::Radians; + RaisePropertyChanged(L"TrigModeRadians"); + RaisePropertyChanged(L"TrigModeDegrees"); + RaisePropertyChanged(L"TrigModeGradians"); + } + } + } + + property bool TrigModeDegrees + { + bool get() + { + return m_Graph != nullptr && m_Graph->TrigUnitMode == (int)Graphing::EvalTrigUnitMode::Degrees; + } + void set(bool value) + { + if (value && m_Graph != nullptr && m_Graph->TrigUnitMode != (int)Graphing::EvalTrigUnitMode::Degrees) + { + m_Graph->TrigUnitMode = (int)Graphing::EvalTrigUnitMode::Degrees; + RaisePropertyChanged(L"TrigModeDegrees"); + RaisePropertyChanged(L"TrigModeRadians"); + RaisePropertyChanged(L"TrigModeGradians"); + } + } + } + + property bool TrigModeGradians + { + bool get() + { + return m_Graph != nullptr && m_Graph->TrigUnitMode == (int)Graphing::EvalTrigUnitMode::Grads; + } + void set(bool value) + { + if (value && m_Graph != nullptr && m_Graph->TrigUnitMode != (int)Graphing::EvalTrigUnitMode::Grads) + { + m_Graph->TrigUnitMode = (int)Graphing::EvalTrigUnitMode::Grads; + RaisePropertyChanged(L"TrigModeGradians"); + RaisePropertyChanged(L"TrigModeDegrees"); + RaisePropertyChanged(L"TrigModeRadians"); + } + } + } + + public: + void UpdateDisplayRange(bool XValuesModified); + + public: + void SetGrapher(GraphControl::Grapher ^ grapher); + void InitRanges(); + bool HasError(); + + private: + Platform::String ^ m_XMin; + Platform::String ^ m_XMax; + Platform::String ^ m_YMin; + Platform::String ^ m_YMax; + double m_XMinValue; + double m_XMaxValue; + double m_YMinValue; + double m_YMaxValue; + bool m_dontUpdateDisplayRange; + bool m_XIsMinLastChanged; + bool m_YIsMinLastChanged; + }; +} diff --git a/src/CalcViewModel/GraphingCalculator/VariableViewModel.h b/src/CalcViewModel/GraphingCalculator/VariableViewModel.h new file mode 100644 index 000000000..d4dd699e3 --- /dev/null +++ b/src/CalcViewModel/GraphingCalculator/VariableViewModel.h @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "../Common/Utils.h" +#include "EquationViewModel.h" + +namespace CalculatorApp::ViewModel +{ +public + value struct VariableChangedEventArgs sealed + { + Platform::String ^ variableName; + double newValue; + }; + + [Windows::UI::Xaml::Data::Bindable] public ref class VariableViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + VariableViewModel(Platform::String ^ name, double value) + : m_Name(name) + , m_Value(value) + , m_SliderSettingsVisible(false) + , m_Min(0.0) + , m_Step(0.1) + , m_Max(2.0) + { + } + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_R(Platform::String ^, Name); + OBSERVABLE_PROPERTY_RW(double, Min); + OBSERVABLE_PROPERTY_RW(double, Step); + OBSERVABLE_PROPERTY_RW(double, Max); + OBSERVABLE_PROPERTY_RW(bool, SliderSettingsVisible); + + event Windows::Foundation::EventHandler ^ VariableUpdated; + + property double Value + { + double get() + { + return m_Value; + } + void set(double value) + { + if (value < Min) + { + value = Min; + } + else if (value > Max) + { + value = Max; + } + + if (Value != value) + { + m_Value = value; + VariableUpdated(this, VariableChangedEventArgs{ Name, value }); + RaisePropertyChanged(L"Value"); + } + } + } + + private: + double m_Value; + }; +} diff --git a/src/CalcViewModel/GraphingCalculatorEnums.h b/src/CalcViewModel/GraphingCalculatorEnums.h new file mode 100644 index 000000000..5cf136a5a --- /dev/null +++ b/src/CalcViewModel/GraphingCalculatorEnums.h @@ -0,0 +1,28 @@ +#pragma once + +namespace CalculatorApp +{ + enum KeyGraphFeaturesFlag + { + Domain = 1, + Range = 2, + Parity = 4, + Periodicity = 8, + Zeros = 16, + YIntercept = 32, + Minima = 64, + Maxima = 128, + InflectionPoints = 256, + VerticalAsymptotes = 512, + HorizontalAsymptotes = 1024, + ObliqueAsymptotes = 2048, + MonotoneIntervals = 4096 + }; + + enum AnalysisErrorType + { + NoError, + AnalysisCouldNotBePerformed, + AnalysisNotSupported + }; +} diff --git a/src/CalcViewModel/ViewState.cpp b/src/CalcViewModel/ViewState.cpp new file mode 100644 index 000000000..63a5d30e3 --- /dev/null +++ b/src/CalcViewModel/ViewState.cpp @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "ViewState.h" + +namespace CalculatorApp +{ + namespace ViewState + { + Platform::StringReference Snap(L"Snap"); + Platform::StringReference DockedView(L"DockedView"); + + bool IsValidViewState(Platform::String ^ viewState) + { + return viewState->Equals(ViewState::Snap) || viewState->Equals(ViewState::DockedView); + } + } +} diff --git a/src/CalcViewModel/ViewState.h b/src/CalcViewModel/ViewState.h new file mode 100644 index 000000000..0a103d097 --- /dev/null +++ b/src/CalcViewModel/ViewState.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace ViewState + { + extern Platform::StringReference Snap; + extern Platform::StringReference DockedView; + + bool IsValidViewState(Platform::String ^ viewState); + } +} diff --git a/src/CalcViewModel/pch.h b/src/CalcViewModel/pch.h index 77e1094bd..40a440aa8 100644 --- a/src/CalcViewModel/pch.h +++ b/src/CalcViewModel/pch.h @@ -40,6 +40,7 @@ #include "winrt/Windows.Globalization.DateTimeFormatting.h" #include "winrt/Windows.System.UserProfile.h" #include "winrt/Windows.UI.Xaml.h" +#include "winrt/Windows.Foundation.Metadata.h" // The following namespaces exist as a convenience to resolve // ambiguity for Windows types in the Windows::UI::Xaml::Automation::Peers diff --git a/src/Calculator.sln b/src/Calculator.sln index aef0d881f..717c6b8c9 100644 --- a/src/Calculator.sln +++ b/src/Calculator.sln @@ -21,6 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CalculatorUITests", "Calcul EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CalculatorUITestFramework", "CalculatorUITestFramework\CalculatorUITestFramework.csproj", "{96454213-94AF-457D-9DF9-B14F80E7770F}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GraphingImpl", "GraphingImpl\GraphingImpl.vcxproj", "{52E03A58-B378-4F50-8BFB-F659FB85E790}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GraphControl", "GraphControl\GraphControl.vcxproj", "{E727A92B-F149-492C-8117-C039A298719B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM = Debug|ARM @@ -137,6 +141,38 @@ Global {96454213-94AF-457D-9DF9-B14F80E7770F}.Release|x64.Build.0 = Release|Any CPU {96454213-94AF-457D-9DF9-B14F80E7770F}.Release|x86.ActiveCfg = Release|Any CPU {96454213-94AF-457D-9DF9-B14F80E7770F}.Release|x86.Build.0 = Release|Any CPU + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|ARM.ActiveCfg = Debug|ARM + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|ARM.Build.0 = Debug|ARM + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|ARM64.Build.0 = Debug|ARM64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|x64.ActiveCfg = Debug|x64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|x64.Build.0 = Debug|x64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|x86.ActiveCfg = Debug|Win32 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Debug|x86.Build.0 = Debug|Win32 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|ARM.ActiveCfg = Release|ARM + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|ARM.Build.0 = Release|ARM + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|ARM64.ActiveCfg = Release|ARM64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|ARM64.Build.0 = Release|ARM64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|x64.ActiveCfg = Release|x64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|x64.Build.0 = Release|x64 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|x86.ActiveCfg = Release|Win32 + {52E03A58-B378-4F50-8BFB-F659FB85E790}.Release|x86.Build.0 = Release|Win32 + {E727A92B-F149-492C-8117-C039A298719B}.Debug|ARM.ActiveCfg = Debug|ARM + {E727A92B-F149-492C-8117-C039A298719B}.Debug|ARM.Build.0 = Debug|ARM + {E727A92B-F149-492C-8117-C039A298719B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E727A92B-F149-492C-8117-C039A298719B}.Debug|ARM64.Build.0 = Debug|ARM64 + {E727A92B-F149-492C-8117-C039A298719B}.Debug|x64.ActiveCfg = Debug|x64 + {E727A92B-F149-492C-8117-C039A298719B}.Debug|x64.Build.0 = Debug|x64 + {E727A92B-F149-492C-8117-C039A298719B}.Debug|x86.ActiveCfg = Debug|Win32 + {E727A92B-F149-492C-8117-C039A298719B}.Debug|x86.Build.0 = Debug|Win32 + {E727A92B-F149-492C-8117-C039A298719B}.Release|ARM.ActiveCfg = Release|ARM + {E727A92B-F149-492C-8117-C039A298719B}.Release|ARM.Build.0 = Release|ARM + {E727A92B-F149-492C-8117-C039A298719B}.Release|ARM64.ActiveCfg = Release|ARM64 + {E727A92B-F149-492C-8117-C039A298719B}.Release|ARM64.Build.0 = Release|ARM64 + {E727A92B-F149-492C-8117-C039A298719B}.Release|x64.ActiveCfg = Release|x64 + {E727A92B-F149-492C-8117-C039A298719B}.Release|x64.Build.0 = Release|x64 + {E727A92B-F149-492C-8117-C039A298719B}.Release|x86.ActiveCfg = Release|Win32 + {E727A92B-F149-492C-8117-C039A298719B}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Calculator/App.xaml b/src/Calculator/App.xaml index f27dcdf8b..3f91ce435 100644 --- a/src/Calculator/App.xaml +++ b/src/Calculator/App.xaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Controls="using:CalculatorApp.Controls" xmlns:common="using:CalculatorApp.Common" - xmlns:converters="using:CalculatorApp.Converters" xmlns:local="using:CalculatorApp"> @@ -28,8 +27,12 @@ Opacity="0.4" Color="{ThemeResource SystemAltMediumLowColor}"/> - - + + @@ -44,6 +47,7 @@ Opacity="0.9" Color="{ThemeResource SystemAccentColor}"/> + + Dark + + + + + + + + + + + + + + + + 0,0,0,0 @@ -80,11 +101,16 @@ Opacity="0.4" Color="{ThemeResource SystemAltMediumLowColor}"/> - - + + + + Light + + + + + + + + + + + + + + + 0,1,0,0 @@ -136,6 +178,7 @@ + @@ -146,6 +189,12 @@ + + Dark + + + + @@ -251,7 +300,6 @@ - - + + + + + + + + diff --git a/src/Calculator/Assets/CalcMDL2.ttf b/src/Calculator/Assets/CalcMDL2.ttf index 9e6bfaa4d..62461d0bc 100644 Binary files a/src/Calculator/Assets/CalcMDL2.ttf and b/src/Calculator/Assets/CalcMDL2.ttf differ diff --git a/src/Calculator/Calculator.vcxproj b/src/Calculator/Calculator.vcxproj index 14ab60369..00043a1ee 100644 --- a/src/Calculator/Calculator.vcxproj +++ b/src/Calculator/Calculator.vcxproj @@ -130,6 +130,9 @@ + + + /bigobj /await /std:c++17 /utf-8 @@ -222,6 +225,9 @@ 0.0.0.0 + + true + @@ -234,6 +240,7 @@ + @@ -251,10 +258,16 @@ + App.xaml + + EquationStylePanelControl.xaml + + + Views\Calculator.xaml @@ -279,6 +292,21 @@ Views\CalculatorStandardOperators.xaml + + Views\GraphingCalculator\EquationInputArea.xaml + + + Views\GraphingCalculator\GraphingCalculator.xaml + + + Views\GraphingCalculator\GraphingSettings.xaml + + + Views\GraphingCalculator\KeyGraphFeaturesPanel.xaml + + + Views\GraphingCalculator\GraphingNumPad.xaml + Views\HistoryList.xaml @@ -318,6 +346,9 @@ Designer + + Designer + Designer @@ -335,6 +366,11 @@ + + + + + @@ -368,6 +404,7 @@ + @@ -384,6 +421,7 @@ + Create Create @@ -394,6 +432,11 @@ Create Create + + EquationStylePanelControl.xaml + + + Views\Calculator.xaml @@ -418,6 +461,21 @@ Views\CalculatorStandardOperators.xaml + + Views\GraphingCalculator\EquationInputArea.xaml + + + Views\GraphingCalculator\GraphingCalculator.xaml + + + Views\GraphingCalculator\GraphingSettings.xaml + + + Views\GraphingCalculator\KeyGraphFeaturesPanel.xaml + + + Views\GraphingCalculator\GraphingNumPad.xaml + Views\HistoryList.xaml @@ -811,6 +869,9 @@ {90e9761d-9262-4773-942d-caeae75d7140} + + {e727a92b-f149-492c-8117-c039a298719b} + diff --git a/src/Calculator/Calculator.vcxproj.filters b/src/Calculator/Calculator.vcxproj.filters index 328c8bab2..742109685 100644 --- a/src/Calculator/Calculator.vcxproj.filters +++ b/src/Calculator/Calculator.vcxproj.filters @@ -218,6 +218,12 @@ {0120c344-0bc0-4a1d-b82c-df7945f46189} + + {e23e2a6e-491b-4200-9bf7-d355a1ee695b} + + + {b491a249-26b8-4814-9f50-2c3a57018c56} + @@ -305,6 +311,19 @@ Controls + + + + Controls + + + + Views\GraphingCalculator + + + + Utils + Common @@ -396,6 +415,19 @@ Controls + + + + Controls + + + + Views\GraphingCalculator + + + + Utils + Common @@ -470,6 +502,24 @@ Views + + Views\GraphingCalculator + + + Views\GraphingCalculator + + + Views + + + Views\GraphingCalculator + + + Views\GraphingCalculator + + + Views\GraphingCalculator + @@ -1516,4 +1566,8 @@ + + + + \ No newline at end of file diff --git a/src/Calculator/Common/KeyboardShortcutManager.cpp b/src/Calculator/Common/KeyboardShortcutManager.cpp index d828a8647..d5c78c05e 100644 --- a/src/Calculator/Common/KeyboardShortcutManager.cpp +++ b/src/Calculator/Common/KeyboardShortcutManager.cpp @@ -10,6 +10,7 @@ using namespace Concurrency; using namespace Platform; using namespace std; +using namespace std::chrono; using namespace Windows::ApplicationModel::Resources; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; @@ -43,10 +44,15 @@ static multimap> s_VirtualKeyControlS static multimap> s_VirtualKeyInverseChordsForButtons; static multimap> s_VirtualKeyControlInverseChordsForButtons; -static multimap s_ShiftKeyPressed; -static multimap s_ControlKeyPressed; -static multimap s_ShiftButtonChecked; -static multimap s_IsDropDownOpen; +static map s_ShiftKeyPressed; +static map s_ControlKeyPressed; +static map s_ShiftButtonChecked; +static map s_IsDropDownOpen; + +static map s_ignoreNextEscape; +static map s_keepIgnoringEscape; +static map s_fHonorShortcuts; +static map s_AboutFlyout; static reader_writer_lock s_keyboardShortcutMapLock; @@ -157,12 +163,6 @@ namespace CalculatorApp } } -static multimap s_ignoreNextEscape; -static multimap s_keepIgnoringEscape; -static multimap s_fHonorShortcuts; -static multimap s_fHandledEnter; -static multimap s_AboutFlyout; - void KeyboardShortcutManager::IgnoreEscape(bool onlyOnce) { // Writer lock for the static maps @@ -172,14 +172,12 @@ void KeyboardShortcutManager::IgnoreEscape(bool onlyOnce) if (s_ignoreNextEscape.find(viewId) != s_ignoreNextEscape.end()) { - s_ignoreNextEscape.erase(viewId); - s_ignoreNextEscape.insert(std::make_pair(viewId, true)); + s_ignoreNextEscape[viewId] = true; } if (s_keepIgnoringEscape.find(viewId) != s_keepIgnoringEscape.end()) { - s_keepIgnoringEscape.erase(viewId); - s_keepIgnoringEscape.insert(std::make_pair(viewId, !onlyOnce)); + s_keepIgnoringEscape[viewId] = !onlyOnce; } } @@ -192,14 +190,12 @@ void KeyboardShortcutManager::HonorEscape() if (s_ignoreNextEscape.find(viewId) != s_ignoreNextEscape.end()) { - s_ignoreNextEscape.erase(viewId); - s_ignoreNextEscape.insert(std::make_pair(viewId, false)); + s_ignoreNextEscape[viewId] = false; } if (s_keepIgnoringEscape.find(viewId) != s_keepIgnoringEscape.end()) { - s_keepIgnoringEscape.erase(viewId); - s_keepIgnoringEscape.insert(std::make_pair(viewId, false)); + s_keepIgnoringEscape[viewId] = false; } } @@ -430,7 +426,6 @@ void KeyboardShortcutManager::OnCharacterReceivedHandler(CoreWindow ^ sender, Ch { wchar_t character = static_cast(args->KeyCode); auto buttons = s_CharacterForButtons.find(viewId)->second.equal_range(character); - RunFirstEnabledButtonCommand(buttons); LightUpButtons(buttons); @@ -474,8 +469,8 @@ const std::multimap& GetCurrentKeyDictionary(MyVirt } else { - auto iterViewMap = s_VirtualKeyControlInverseChordsForButtons.find(viewId); - if (iterViewMap != s_VirtualKeyControlInverseChordsForButtons.end()) + auto iterViewMap = s_VirtualKeyInverseChordsForButtons.find(viewId); + if (iterViewMap != s_VirtualKeyInverseChordsForButtons.end()) { for (auto iterator = iterViewMap->second.begin(); iterator != iterViewMap->second.end(); ++iterator) { @@ -558,8 +553,7 @@ void KeyboardShortcutManager::OnKeyDownHandler(CoreWindow ^ sender, KeyEventArgs if (currControlKeyPressed != s_ControlKeyPressed.end()) { - s_ControlKeyPressed.erase(viewId); - s_ControlKeyPressed.insert(std::make_pair(viewId, true)); + s_ControlKeyPressed[viewId] = true; } return; } @@ -572,26 +566,24 @@ void KeyboardShortcutManager::OnKeyDownHandler(CoreWindow ^ sender, KeyEventArgs if (currShiftKeyPressed != s_ShiftKeyPressed.end()) { - s_ShiftKeyPressed.erase(viewId); - s_ShiftKeyPressed.insert(std::make_pair(viewId, true)); + s_ShiftKeyPressed[viewId] = true; } return; } - const auto& lookupMap = GetCurrentKeyDictionary(static_cast(key)); - auto buttons = lookupMap.equal_range(static_cast(key)); - - auto currentIsDropDownOpen = s_IsDropDownOpen.find(viewId); - if (currentHonorShortcuts != s_fHonorShortcuts.end()) { if (currentHonorShortcuts->second) { + const auto myVirtualKey = static_cast(key); + const auto& lookupMap = GetCurrentKeyDictionary(myVirtualKey); + auto buttons = lookupMap.equal_range(myVirtualKey); RunFirstEnabledButtonCommand(buttons); // Ctrl+C and Ctrl+V shifts focus to some button because of which enter doesn't work after copy/paste. So don't shift focus if Ctrl+C or Ctrl+V // is pressed. When drop down is open, pressing escape shifts focus to clear button. So dont's shift focus if drop down is open. Ctrl+Insert is // equivalent to Ctrl+C and Shift+Insert is equivalent to Ctrl+V + auto currentIsDropDownOpen = s_IsDropDownOpen.find(viewId); if (currentIsDropDownOpen != s_IsDropDownOpen.end() && !currentIsDropDownOpen->second) { // Do not Light Up Buttons when Ctrl+C, Ctrl+V, Ctrl+Insert or Shift+Insert is pressed @@ -620,8 +612,7 @@ void KeyboardShortcutManager::OnKeyUpHandler(CoreWindow ^ sender, KeyEventArgs ^ if (currentShiftKeyPressed != s_ShiftKeyPressed.end()) { - s_ShiftKeyPressed.erase(viewId); - s_ShiftKeyPressed.insert(std::make_pair(viewId, false)); + s_ShiftKeyPressed[viewId] = false; } } else if (key == VirtualKey::Control) @@ -633,8 +624,7 @@ void KeyboardShortcutManager::OnKeyUpHandler(CoreWindow ^ sender, KeyEventArgs ^ if (currControlKeyPressed != s_ControlKeyPressed.end()) { - s_ControlKeyPressed.erase(viewId); - s_ControlKeyPressed.insert(std::make_pair(viewId, false)); + s_ControlKeyPressed[viewId] = false; } } } @@ -712,8 +702,7 @@ void KeyboardShortcutManager::ShiftButtonChecked(bool checked) if (s_ShiftButtonChecked.find(viewId) != s_ShiftButtonChecked.end()) { - s_ShiftButtonChecked.erase(viewId); - s_ShiftButtonChecked.insert(std::make_pair(viewId, checked)); + s_ShiftButtonChecked[viewId] = checked; } } @@ -723,8 +712,7 @@ void KeyboardShortcutManager::UpdateDropDownState(bool isOpen) if (s_IsDropDownOpen.find(viewId) != s_IsDropDownOpen.end()) { - s_IsDropDownOpen.erase(viewId); - s_IsDropDownOpen.insert(std::make_pair(viewId, isOpen)); + s_IsDropDownOpen[viewId] = isOpen; } } @@ -734,8 +722,7 @@ void KeyboardShortcutManager::UpdateDropDownState(Flyout ^ aboutPageFlyout) if (s_AboutFlyout.find(viewId) != s_AboutFlyout.end()) { - s_AboutFlyout.erase(viewId); - s_AboutFlyout.insert(std::make_pair(viewId, aboutPageFlyout)); + s_AboutFlyout[viewId] = aboutPageFlyout; } } @@ -748,19 +735,7 @@ void KeyboardShortcutManager::HonorShortcuts(bool allow) if (s_fHonorShortcuts.find(viewId) != s_fHonorShortcuts.end()) { - s_fHonorShortcuts.erase(viewId); - s_fHonorShortcuts.insert(std::make_pair(viewId, allow)); - } -} - -void KeyboardShortcutManager::HandledEnter(bool ishandled) -{ - int viewId = Utils::GetWindowId(); - - if (s_fHandledEnter.find(viewId) != s_fHandledEnter.end()) - { - s_fHandledEnter.erase(viewId); - s_fHandledEnter.insert(std::make_pair(viewId, ishandled)); + s_fHonorShortcuts[viewId] = allow; } } @@ -812,15 +787,14 @@ void KeyboardShortcutManager::RegisterNewAppViewId() s_VirtualKeyControlInverseChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); } - s_ShiftKeyPressed.insert(std::make_pair(appViewId, false)); - s_ControlKeyPressed.insert(std::make_pair(appViewId, false)); - s_ShiftButtonChecked.insert(std::make_pair(appViewId, false)); - s_IsDropDownOpen.insert(std::make_pair(appViewId, false)); - s_ignoreNextEscape.insert(std::make_pair(appViewId, false)); - s_keepIgnoringEscape.insert(std::make_pair(appViewId, false)); - s_fHonorShortcuts.insert(std::make_pair(appViewId, true)); - s_fHandledEnter.insert(std::make_pair(appViewId, true)); - s_AboutFlyout.insert(std::make_pair(appViewId, nullptr)); + s_ShiftKeyPressed[appViewId] = false; + s_ControlKeyPressed[appViewId] = false; + s_ShiftButtonChecked[appViewId] = false; + s_IsDropDownOpen[appViewId] = false; + s_ignoreNextEscape[appViewId] = false; + s_keepIgnoringEscape[appViewId] = false; + s_fHonorShortcuts[appViewId] = true; + s_AboutFlyout[appViewId] = nullptr; } void KeyboardShortcutManager::OnWindowClosed(int viewId) @@ -845,6 +819,5 @@ void KeyboardShortcutManager::OnWindowClosed(int viewId) s_ignoreNextEscape.erase(viewId); s_keepIgnoringEscape.erase(viewId); s_fHonorShortcuts.erase(viewId); - s_fHandledEnter.erase(viewId); s_AboutFlyout.erase(viewId); } diff --git a/src/Calculator/Common/KeyboardShortcutManager.h b/src/Calculator/Common/KeyboardShortcutManager.h index 0d6fce556..124ae167e 100644 --- a/src/Calculator/Common/KeyboardShortcutManager.h +++ b/src/Calculator/Common/KeyboardShortcutManager.h @@ -43,7 +43,6 @@ namespace CalculatorApp static void IgnoreEscape(bool onlyOnce); static void HonorEscape(); static void HonorShortcuts(bool allow); - static void HandledEnter(bool ishandled); static void UpdateDropDownState(bool); static void ShiftButtonChecked(bool checked); static void UpdateDropDownState(Windows::UI::Xaml::Controls::Flyout ^ aboutPageFlyout); diff --git a/src/Calculator/Controls/EquationTextBox.cpp b/src/Calculator/Controls/EquationTextBox.cpp new file mode 100644 index 000000000..cc873ca5a --- /dev/null +++ b/src/Calculator/Controls/EquationTextBox.cpp @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "CalcViewModel/Common/AppResourceProvider.h" +#include "CalcViewModel/Common/LocalizationStringUtil.h" +#include "EquationTextBox.h" + +using namespace std; +using namespace Platform; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Controls; +using namespace Windows::System; +using namespace Windows::Foundation; +using namespace Windows::ApplicationModel; +using namespace Windows::UI::Text; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Controls::Primitives; + +DEPENDENCY_PROPERTY_INITIALIZATION(EquationTextBox, EquationColor); +DEPENDENCY_PROPERTY_INITIALIZATION(EquationTextBox, ColorChooserFlyout); +DEPENDENCY_PROPERTY_INITIALIZATION(EquationTextBox, EquationButtonContentIndex); +DEPENDENCY_PROPERTY_INITIALIZATION(EquationTextBox, HasError); +DEPENDENCY_PROPERTY_INITIALIZATION(EquationTextBox, IsAddEquationMode); +DEPENDENCY_PROPERTY_INITIALIZATION(EquationTextBox, MathEquation); + +EquationTextBox::EquationTextBox() +{ +} + +void EquationTextBox::OnApplyTemplate() +{ + m_equationButton = dynamic_cast(GetTemplateChild("EquationButton")); + m_richEditBox = dynamic_cast(GetTemplateChild("MathRichEditBox")); + m_deleteButton = dynamic_cast