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(GetTemplateChild("DeleteButton")); + m_removeButton = dynamic_cast(GetTemplateChild("RemoveButton")); + m_functionButton = dynamic_cast(GetTemplateChild("FunctionButton")); + m_colorChooserButton = dynamic_cast(GetTemplateChild("ColorChooserButton")); + m_richEditContextMenu = dynamic_cast(GetTemplateChild("MathRichEditContextMenu")); + m_kgfEquationMenuItem = dynamic_cast(GetTemplateChild("FunctionAnalysisMenuItem")); + m_removeMenuItem = dynamic_cast(GetTemplateChild("RemoveFunctionMenuItem")); + m_colorChooserMenuItem = dynamic_cast(GetTemplateChild("ChangeFunctionStyleMenuItem")); + + auto resProvider = AppResourceProvider::GetInstance(); + + if (m_richEditBox != nullptr) + { + m_richEditBox->GotFocus += ref new RoutedEventHandler(this, &EquationTextBox::OnRichEditBoxGotFocus); + m_richEditBox->LostFocus += ref new RoutedEventHandler(this, &EquationTextBox::OnRichEditBoxLostFocus); + m_richEditBox->SelectionFlyout = nullptr; + m_richEditBox->EquationSubmitted += + ref new EventHandler(this, &EquationTextBox::OnEquationSubmitted); + } + + if (m_equationButton != nullptr) + { + m_equationButton->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnEquationButtonClicked); + + auto toolTip = ref new ToolTip(); + auto equationButtonMessage = m_equationButton->IsChecked->Value ? resProvider->GetResourceString(L"showEquationButtonToolTip") + : resProvider->GetResourceString(L"hideEquationButtonToolTip"); + toolTip->Content = equationButtonMessage; + ToolTipService::SetToolTip(m_equationButton, toolTip); + AutomationProperties::SetName(m_equationButton, equationButtonMessage); + } + + if (m_richEditContextMenu != nullptr) + { + m_richEditContextMenu->Opening += ref new EventHandler(this, &EquationTextBox::OnRichEditMenuOpening); + } + + if (m_deleteButton != nullptr) + { + m_deleteButton->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnDeleteButtonClicked); + } + + if (m_removeButton != nullptr) + { + m_removeButton->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnRemoveButtonClicked); + } + + if (m_removeMenuItem != nullptr) + { + m_removeMenuItem->Text = resProvider->GetResourceString(L"removeMenuItem"); + m_removeMenuItem->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnRemoveButtonClicked); + } + + if (m_colorChooserButton != nullptr) + { + m_colorChooserButton->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnColorChooserButtonClicked); + } + + if (m_colorChooserMenuItem != nullptr) + { + m_colorChooserMenuItem->Text = resProvider->GetResourceString(L"colorChooserMenuItem"); + m_colorChooserMenuItem->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnColorChooserButtonClicked); + } + + if (m_functionButton != nullptr) + { + m_functionButton->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnFunctionButtonClicked); + m_functionButton->IsEnabled = false; + } + + if (m_kgfEquationMenuItem != nullptr) + { + m_kgfEquationMenuItem->Text = resProvider->GetResourceString(L"functionAnalysisMenuItem"); + m_kgfEquationMenuItem->Click += ref new RoutedEventHandler(this, &EquationTextBox::OnFunctionButtonClicked); + } + + if (ColorChooserFlyout != nullptr) + { + ColorChooserFlyout->Opened += ref new EventHandler(this, &EquationTextBox::OnColorFlyoutOpened); + ColorChooserFlyout->Closed += ref new EventHandler(this, &EquationTextBox::OnColorFlyoutClosed); + } + + UpdateCommonVisualState(); + UpdateButtonsVisualState(); +} + +void EquationTextBox::OnPointerEntered(PointerRoutedEventArgs ^ e) +{ + m_isPointerOver = true; + UpdateCommonVisualState(); +} + +void EquationTextBox::OnPointerExited(PointerRoutedEventArgs ^ e) +{ + m_isPointerOver = false; + UpdateCommonVisualState(); +} + +void EquationTextBox::OnPointerCanceled(PointerRoutedEventArgs ^ e) +{ + m_isPointerOver = false; + UpdateCommonVisualState(); +} + +void EquationTextBox::OnPointerCaptureLost(PointerRoutedEventArgs ^ e) +{ + m_isPointerOver = false; + UpdateCommonVisualState(); +} + +void EquationTextBox::OnColorFlyoutOpened(Object ^ sender, Object ^ e) +{ + m_isColorChooserFlyoutOpen = true; + UpdateCommonVisualState(); +} + +void EquationTextBox::OnColorFlyoutClosed(Object ^ sender, Object ^ e) +{ + m_colorChooserButton->IsChecked = false; + m_isColorChooserFlyoutOpen = false; + UpdateCommonVisualState(); +} + +void EquationTextBox::OnRichEditBoxGotFocus(Object ^ sender, RoutedEventArgs ^ e) +{ + m_HasFocus = true; + UpdateCommonVisualState(); + UpdateButtonsVisualState(); +} + +void EquationTextBox::OnRichEditBoxLostFocus(Object ^ sender, RoutedEventArgs ^ e) +{ + if (!m_richEditBox->ContextFlyout->IsOpen) + { + m_HasFocus = false; + } + + UpdateCommonVisualState(); + UpdateButtonsVisualState(); +} + +void EquationTextBox::OnDeleteButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + if (m_richEditBox != nullptr) + { + m_richEditBox->MathText = L""; + if (m_functionButton) + { + m_functionButton->IsEnabled = false; + } + } +} + +void EquationTextBox::OnEquationButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + EquationButtonClicked(this, ref new RoutedEventArgs()); + + auto toolTip = ref new ToolTip(); + auto resProvider = AppResourceProvider::GetInstance(); + auto equationButtonMessage = m_equationButton->IsChecked->Value ? resProvider->GetResourceString(L"showEquationButtonToolTip") + : resProvider->GetResourceString(L"hideEquationButtonToolTip"); + toolTip->Content = equationButtonMessage; + ToolTipService::SetToolTip(m_equationButton, toolTip); + AutomationProperties::SetName(m_equationButton, equationButtonMessage); +} + +void EquationTextBox::OnRemoveButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + if (IsAddEquationMode) + { + // Don't remove the last equation + return; + } + + if (m_richEditBox != nullptr) + { + m_richEditBox->MathText = L""; + } + + RemoveButtonClicked(this, ref new RoutedEventArgs()); + + if (m_functionButton) + { + m_functionButton->IsEnabled = false; + } + + if (m_equationButton) + { + m_equationButton->IsChecked = false; + } + + VisualStateManager::GoToState(this, "Normal", true); +} + +void EquationTextBox::OnColorChooserButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + if (ColorChooserFlyout != nullptr && m_richEditBox != nullptr) + { + ColorChooserFlyout->ShowAt(m_richEditBox); + } +} + +void EquationTextBox::OnFunctionButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + KeyGraphFeaturesButtonClicked(this, ref new RoutedEventArgs()); +} + +void EquationTextBox::UpdateButtonsVisualState() +{ + String ^ state; + + if (IsAddEquationMode) + { + state = "ButtonHideRemove"; + } + else if (RichEditHasContent()) + { + state = "ButtonVisible"; + } + else + { + state = "ButtonCollapsed"; + } + + VisualStateManager::GoToState(this, state, true); +} + +void EquationTextBox::UpdateCommonVisualState() +{ + String ^ state = nullptr; + + if (m_HasFocus && HasError) + { + state = "FocusedError"; + } + else if (m_HasFocus) + { + state = "Focused"; + } + else if (HasError && (m_isPointerOver || m_isColorChooserFlyoutOpen)) + { + state = "PointerOverError"; + } + else if (m_isPointerOver || m_isColorChooserFlyoutOpen) + { + state = "PointerOver"; + } + else if (HasError) + { + state = "Error"; + } + else if (IsAddEquationMode) + { + state = "AddEquation"; + } + else + { + state = "Normal"; + } + VisualStateManager::GoToState(this, state, false); +} + +void EquationTextBox::OnHasErrorPropertyChanged(bool, bool) +{ + UpdateCommonVisualState(); +} + +Platform::String ^ EquationTextBox::GetEquationText() +{ + String ^ text; + if (m_richEditBox != nullptr) + { + // Clear formatting since the graph control doesn't work with bold/underlines + ITextRange ^ range = m_richEditBox->TextDocument->GetRange(0, m_richEditBox->TextDocument->Selection->EndPosition); + + if (range != nullptr) + { + range->CharacterFormat->Bold = FormatEffect::Off; + range->CharacterFormat->Underline = UnderlineType::None; + } + + text = m_richEditBox->MathText; + } + + return text; +} + +void EquationTextBox::SetEquationText(Platform::String ^ equationText) +{ + if (m_richEditBox != nullptr) + { + m_richEditBox->MathText = equationText; + } +} + +bool EquationTextBox::RichEditHasContent() +{ + String ^ text; + + if (m_richEditBox != nullptr) + { + text = m_richEditBox->MathText; + } + return !text->IsEmpty() && m_HasFocus; +} + +void EquationTextBox::OnRichEditMenuOpening(Object ^ /*sender*/, Object ^ /*args*/) +{ + if (m_kgfEquationMenuItem != nullptr) + { + m_kgfEquationMenuItem->IsEnabled = RichEditHasContent(); + } + + if (m_colorChooserMenuItem != nullptr) + { + m_colorChooserMenuItem->IsEnabled = !HasError; + } +} + +void EquationTextBox::OnIsAddEquationModePropertyChanged(bool /*oldValue*/, bool /*newValue*/) +{ + UpdateCommonVisualState(); + UpdateButtonsVisualState(); +} + +void EquationTextBox::FocusTextBox() +{ + if (m_richEditBox != nullptr) + { + FocusManager::TryFocusAsync(m_richEditBox, ::FocusState::Programmatic); + } +} + +void EquationTextBox::OnEquationSubmitted(Platform::Object ^ sender, MathRichEditBoxSubmission ^ args) +{ + if (args->HasTextChanged) + { + if (m_functionButton && m_richEditBox->MathText != L"") + { + m_functionButton->IsEnabled = true; + } + } + + EquationSubmitted(this, args); +} diff --git a/src/Calculator/Controls/EquationTextBox.h b/src/Calculator/Controls/EquationTextBox.h new file mode 100644 index 000000000..36860252d --- /dev/null +++ b/src/Calculator/Controls/EquationTextBox.h @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "CalcViewModel/Common/Utils.h" +#include "CalcViewModel/GraphingCalculator/EquationViewModel.h" +#include "Calculator/Controls/MathRichEditBox.h" + +namespace CalculatorApp +{ + namespace Controls + { + public + ref class EquationTextBox sealed : public Windows::UI::Xaml::Controls::Control + { + public: + EquationTextBox(); + + DEPENDENCY_PROPERTY_OWNER(EquationTextBox); + DEPENDENCY_PROPERTY(Windows::UI::Xaml::Media::SolidColorBrush ^, EquationColor); + DEPENDENCY_PROPERTY(Windows::UI::Xaml::Controls::Flyout ^, ColorChooserFlyout); + DEPENDENCY_PROPERTY(Platform::String ^, EquationButtonContentIndex); + DEPENDENCY_PROPERTY(Platform::String ^, MathEquation); + DEPENDENCY_PROPERTY_WITH_CALLBACK(bool, HasError); + DEPENDENCY_PROPERTY_WITH_CALLBACK(bool, IsAddEquationMode); + + PROPERTY_R(bool, HasFocus); + + event Windows::UI::Xaml::RoutedEventHandler ^ RemoveButtonClicked; + event Windows::UI::Xaml::RoutedEventHandler ^ KeyGraphFeaturesButtonClicked; + event Windows::Foundation::EventHandler ^ EquationSubmitted; + event Windows::UI::Xaml::RoutedEventHandler ^ EquationButtonClicked; + + Platform::String ^ GetEquationText(); + void SetEquationText(Platform::String ^ equationText); + void FocusTextBox(); + + protected: + virtual void OnApplyTemplate() override; + virtual void OnPointerEntered(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + virtual void OnPointerExited(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + virtual void OnPointerCanceled(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + virtual void OnPointerCaptureLost(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnIsAddEquationModePropertyChanged(bool oldValue, bool newValue); + + private: + void UpdateCommonVisualState(); + void UpdateButtonsVisualState(); + bool RichEditHasContent(); + + void OnRichEditBoxGotFocus(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void OnRichEditBoxLostFocus(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + + void OnDeleteButtonClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnEquationButtonClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnRemoveButtonClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnColorChooserButtonClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnFunctionButtonClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnRichEditMenuOpening(Platform::Object ^ sender, Platform::Object ^ args); + + void OnColorFlyoutOpened(Platform::Object ^ sender, Platform::Object ^ e); + void OnColorFlyoutClosed(Platform::Object ^ sender, Platform::Object ^ e); + + void OnHasErrorPropertyChanged(bool oldValue, bool newValue); + + CalculatorApp::Controls::MathRichEditBox ^ m_richEditBox; + Windows::UI::Xaml::Controls::Primitives::ToggleButton ^ m_equationButton; + Windows::UI::Xaml::Controls::Button ^ m_deleteButton; + Windows::UI::Xaml::Controls::Button ^ m_removeButton; + Windows::UI::Xaml::Controls::Button ^ m_functionButton; + Windows::UI::Xaml::Controls::Primitives::ToggleButton ^ m_colorChooserButton; + + Windows::UI::Xaml::Controls::MenuFlyout^ m_richEditContextMenu; + Windows::UI::Xaml::Controls::MenuFlyoutItem^ m_kgfEquationMenuItem; + Windows::UI::Xaml::Controls::MenuFlyoutItem^ m_removeMenuItem; + Windows::UI::Xaml::Controls::MenuFlyoutItem^ m_colorChooserMenuItem; + + bool m_isPointerOver; + bool m_isColorChooserFlyoutOpen; + void OnEquationSubmitted(Platform::Object ^ sender, CalculatorApp::Controls::MathRichEditBoxSubmission ^ args); + }; + } +} diff --git a/src/Calculator/Controls/MathRichEditBox.cpp b/src/Calculator/Controls/MathRichEditBox.cpp new file mode 100644 index 000000000..b19471a12 --- /dev/null +++ b/src/Calculator/Controls/MathRichEditBox.cpp @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "MathRichEditBox.h" + +using namespace Platform; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Controls; +using namespace std; +using namespace Windows::ApplicationModel; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::Foundation::Collections; +using namespace Windows::System; + +DEPENDENCY_PROPERTY_INITIALIZATION(MathRichEditBox, MathText); + +// TODO remove when Windows 10 version 2004 SDK is adopted +namespace Windows_2004_Prerelease +{ + enum RichEditMathMode : int + { + NoMath, + MathOnly, + }; + MIDL_INTERFACE("619c20f2-cb3b-4521-981f-2865b1b93f04") + ITextDocument4 : public IInspectable + { + public: + virtual HRESULT STDMETHODCALLTYPE SetMath(HSTRING value) = 0; + virtual HRESULT STDMETHODCALLTYPE GetMath(HSTRING * value) = 0; + virtual HRESULT STDMETHODCALLTYPE SetMathMode(RichEditMathMode mathMode) = 0; + }; +} + +MathRichEditBox::MathRichEditBox() +{ + static LimitedAccessFeatureStatus m_lafResultStatus; + String ^ packageName = Package::Current->Id->Name; + + if (packageName == L"Microsoft.WindowsCalculator.Dev") + { + m_lafResultStatus = LimitedAccessFeatures::TryUnlockFeature( + "com.microsoft.windows.richeditmath", + "BeDD/jxKhz/yfVNA11t4uA==", // Microsoft.WindowsCalculator.Dev + "8wekyb3d8bbwe has registered their use of com.microsoft.windows.richeditmath with Microsoft and agrees to the terms of use.") + ->Status; + } + + else if (packageName == L"Microsoft.WindowsCalculator") + { + m_lafResultStatus = LimitedAccessFeatures::TryUnlockFeature( + "com.microsoft.windows.richeditmath", + "pfanNuxnzo+mAkBQ3N/rGQ==", // Microsoft.WindowsCalculator + "8wekyb3d8bbwe has registered their use of com.microsoft.windows.richeditmath with Microsoft and agrees to the terms of use.") + ->Status; + } + + else if (packageName == L"Microsoft.WindowsCalculator.Graphing") + { + m_lafResultStatus = LimitedAccessFeatures::TryUnlockFeature( + "com.microsoft.windows.richeditmath", + "H6wflFFz3gkOsAHtG/D9Tg==", // Microsoft.WindowsCalculator.Graphing + "8wekyb3d8bbwe has registered their use of com.microsoft.windows.richeditmath with Microsoft and agrees to the terms of use.") + ->Status; + } + + // TODO when Windows 10 version 2004 SDK is adopted, replace with: + // TextDocument->SetMathMode(Windows::UI::Text::RichEditMathMode::MathOnly); + Microsoft::WRL::ComPtr textDocument4; + reinterpret_cast(this->TextDocument)->QueryInterface(IID_PPV_ARGS(&textDocument4)); + auto hr = textDocument4->SetMathMode(Windows_2004_Prerelease::RichEditMathMode::MathOnly); + if (FAILED(hr)) + { + throw Exception::CreateException(hr); + } + this->LosingFocus += ref new Windows::Foundation::TypedEventHandler( + this, &CalculatorApp::Controls::MathRichEditBox::OnLosingFocus); + this->KeyUp += ref new Windows::UI::Xaml::Input::KeyEventHandler(this, &CalculatorApp::Controls::MathRichEditBox::OnKeyUp); +} + +String ^ MathRichEditBox::GetMathTextProperty() +{ + // TODO when Windows 10 version 2004 SDK is adopted, replace with: + // String ^ text; + // this->TextDocument->GetMath(&text); + // return text; + + Microsoft::WRL::ComPtr textDocument4; + reinterpret_cast(this->TextDocument)->QueryInterface(IID_PPV_ARGS(&textDocument4)); + HSTRING math; + auto hr = textDocument4->GetMath(&math); + if (FAILED(hr)) + { + throw Exception::CreateException(hr); + } + return reinterpret_cast(math); +} + +void MathRichEditBox::SetMathTextProperty(String ^ newValue) +{ + bool readOnlyState = this->IsReadOnly; + this->IsReadOnly = false; + + // TODO when Windows 10 version 2004 SDK is adopted, replace with: + // TextDocument->SetMath(newValue); + Microsoft::WRL::ComPtr textDocument4; + reinterpret_cast(this->TextDocument)->QueryInterface(IID_PPV_ARGS(&textDocument4)); + auto hr = textDocument4->SetMath(reinterpret_cast(newValue)); + if (FAILED(hr)) + { + throw Exception::CreateException(hr); + } + + this->IsReadOnly = readOnlyState; +} + +void CalculatorApp::Controls::MathRichEditBox::OnLosingFocus(Windows::UI::Xaml::UIElement ^ sender, Windows::UI::Xaml::Input::LosingFocusEventArgs ^ args) +{ + SubmitEquation(EquationSubmissionSource::FOCUS_LOST); +} + +void CalculatorApp::Controls::MathRichEditBox::OnKeyUp(Platform::Object ^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs ^ e) +{ + if (e->Key == VirtualKey::Enter) + { + SubmitEquation(EquationSubmissionSource::ENTER_KEY); + } +} + +void MathRichEditBox::OnMathTextPropertyChanged(Platform::String ^ oldValue, Platform::String ^ newValue) +{ + SetMathTextProperty(newValue); + SetValue(MathTextProperty, newValue); +} + +void MathRichEditBox::InsertText(Platform::String ^ text, int cursorOffSet, int selectionLength) +{ + // If the rich edit is empty, the math zone may not exist, and so selection (and thus the resulting text) will not be in a math zone. + // If the rich edit has content already, then the mathzone will already be created due to mathonly mode being set and the selection will exist inside the + // math zone. To handle this, we will force a math zone to be created in teh case of the text being empty and then replacing the text inside of the math + // zone with the newly inserted text. + if (GetMathTextProperty() == nullptr) + { + SetMathTextProperty("x"); + TextDocument->Selection->StartPosition = 0; + TextDocument->Selection->EndPosition = 1; + } + + // insert the text in place of selection + TextDocument->Selection->SetText(Windows::UI::Text::TextSetOptions::FormatRtf, text); + + // Move the cursor to the next logical place for users to enter text. + TextDocument->Selection->StartPosition += cursorOffSet; + TextDocument->Selection->EndPosition = TextDocument->Selection->StartPosition + selectionLength; +} + +void MathRichEditBox::BackSpace() +{ + // if anything is selected, just delete the selection. Note: EndPosition can be before start position. + if (TextDocument->Selection->StartPosition != TextDocument->Selection->EndPosition) + { + TextDocument->Selection->SetText(Windows::UI::Text::TextSetOptions::None, L""); + return; + } + + // if we are at the start of the string, do nothing + if (TextDocument->Selection->StartPosition == 0) + { + return; + } + + // Select the previous group. + TextDocument->Selection->EndPosition = TextDocument->Selection->StartPosition; + TextDocument->Selection->StartPosition -= 1; + + // If the group contains anything complex, we want to give the user a chance to preview the deletion. + // If it's a single character, then just delete it. Otherwise do nothing until the user triggers backspace again. + auto text = TextDocument->Selection->Text; + if (text->Length() == 1) + { + TextDocument->Selection->SetText(Windows::UI::Text::TextSetOptions::None, L""); + } +} + +void MathRichEditBox::SubmitEquation(EquationSubmissionSource source) +{ + auto newVal = GetMathTextProperty(); + if (MathText != newVal) + { + SetValue(MathTextProperty, newVal); + EquationSubmitted(this, ref new MathRichEditBoxSubmission(true, source)); + } + else + { + EquationSubmitted(this, ref new MathRichEditBoxSubmission(false, source)); + } +} diff --git a/src/Calculator/Controls/MathRichEditBox.h b/src/Calculator/Controls/MathRichEditBox.h new file mode 100644 index 000000000..c4b6d488a --- /dev/null +++ b/src/Calculator/Controls/MathRichEditBox.h @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "CalcViewModel/Common/Utils.h" + +namespace CalculatorApp +{ + namespace Controls + { + public + enum class EquationSubmissionSource + { + FOCUS_LOST, + ENTER_KEY, + PROGRAMMATIC + }; + + public + ref class MathRichEditBoxSubmission sealed + { + public: + PROPERTY_R(bool, HasTextChanged); + PROPERTY_R(EquationSubmissionSource, Source); + public: + MathRichEditBoxSubmission(bool hasTextChanged, EquationSubmissionSource source) + : m_HasTextChanged(hasTextChanged) + , m_Source(source) + { + } + }; + + public + ref class MathRichEditBox sealed : Windows::UI::Xaml::Controls::RichEditBox + { + public: + MathRichEditBox(); + + DEPENDENCY_PROPERTY_OWNER(MathRichEditBox); + DEPENDENCY_PROPERTY_WITH_DEFAULT_AND_CALLBACK(Platform::String ^, MathText, L""); + + event Windows::Foundation::EventHandler ^ EquationSubmitted; + void OnMathTextPropertyChanged(Platform::String ^ oldValue, Platform::String ^ newValue); + void InsertText(Platform::String ^ text, int cursorOffSet, int selectionLength); + void SubmitEquation(EquationSubmissionSource source); + void BackSpace(); + + private: + Platform::String ^ GetMathTextProperty(); + void SetMathTextProperty(Platform::String ^ newValue); + + void OnLosingFocus(Windows::UI::Xaml::UIElement ^ sender, Windows::UI::Xaml::Input::LosingFocusEventArgs ^ args); + void OnKeyUp(Platform::Object ^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs ^ e); + }; + } +} diff --git a/src/Calculator/EquationStylePanelControl.xaml b/src/Calculator/EquationStylePanelControl.xaml new file mode 100644 index 000000000..8eaf5b0e6 --- /dev/null +++ b/src/Calculator/EquationStylePanelControl.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/EquationStylePanelControl.xaml.cpp b/src/Calculator/EquationStylePanelControl.xaml.cpp new file mode 100644 index 000000000..e143d07af --- /dev/null +++ b/src/Calculator/EquationStylePanelControl.xaml.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "EquationStylePanelControl.xaml.h" + +using namespace CalculatorApp; + +using namespace Platform; +using namespace Platform::Collections; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Navigation; +using namespace Windows::UI::Xaml::Shapes; + +DEPENDENCY_PROPERTY_INITIALIZATION(EquationStylePanelControl, SelectedColor); +DEPENDENCY_PROPERTY_INITIALIZATION(EquationStylePanelControl, AvailableColors); + +EquationStylePanelControl::EquationStylePanelControl() +{ + InitializeComponent(); +} + +void EquationStylePanelControl::SelectionChanged(Object ^ /*sender */, SelectionChangedEventArgs ^ e) +{ + if (e->AddedItems->Size > 0) + { + auto brush = dynamic_cast(e->AddedItems->GetAt(0)); + if (brush == nullptr) + { + SelectedColor = Colors::Black; + } + else + { + SelectedColor = brush->Color; + } + } +} + +void EquationStylePanelControl::OnSelectedColorPropertyChanged(Color /*oldColor*/, Color newColor) +{ + SelectColor(newColor); +} + +void EquationStylePanelControl::ColorChooserLoaded(Object ^ sender, RoutedEventArgs ^ e) +{ + SelectColor(SelectedColor); +} + +void EquationStylePanelControl::SelectColor(Color selectedColor) +{ + for (auto item : ColorChooser->Items->GetView()) + { + auto brush = static_cast(item); + auto gridViewItem = dynamic_cast(ColorChooser->ContainerFromItem(brush)); + + if (!gridViewItem) + { + continue; + } + + if (Utils::AreColorsEqual(brush->Color, selectedColor)) + { + gridViewItem->IsSelected = true; + return; + } + else + { + gridViewItem->IsSelected = false; + } + } +} diff --git a/src/Calculator/EquationStylePanelControl.xaml.h b/src/Calculator/EquationStylePanelControl.xaml.h new file mode 100644 index 000000000..b3c3aabb0 --- /dev/null +++ b/src/Calculator/EquationStylePanelControl.xaml.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "EquationStylePanelControl.g.h" +#include "CalcViewModel/Common/Utils.h" + +namespace CalculatorApp +{ + [Windows::Foundation::Metadata::WebHostHidden] public ref class EquationStylePanelControl sealed + { + public: + EquationStylePanelControl(); + DEPENDENCY_PROPERTY_OWNER(EquationStylePanelControl); + + DEPENDENCY_PROPERTY_WITH_DEFAULT_AND_CALLBACK(Windows::UI::Color, SelectedColor, Windows::UI::Colors::Black); + DEPENDENCY_PROPERTY_WITH_DEFAULT(Windows::Foundation::Collections::IVector ^, AvailableColors, nullptr); + + private: + void SelectionChanged(Platform::Object ^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs ^ e); + void OnSelectedColorPropertyChanged(Windows::UI::Color oldColor, Windows::UI::Color newColor); + void ColorChooserLoaded( + Platform::Object ^ sender, + Windows::UI::Xaml::RoutedEventArgs ^ e); + void SelectColor(Windows::UI::Color selectedColor); + }; +} diff --git a/src/Calculator/Package.appxmanifest b/src/Calculator/Package.appxmanifest index 7e493a5f2..76d1c97f0 100644 --- a/src/Calculator/Package.appxmanifest +++ b/src/Calculator/Package.appxmanifest @@ -1,6 +1,6 @@ - + - + ms-resource:DevAppStoreName diff --git a/src/Calculator/Resources/en-US/Resources.resw b/src/Calculator/Resources/en-US/Resources.resw index e15a19a3b..83cff66ca 100644 --- a/src/Calculator/Resources/en-US/Resources.resw +++ b/src/Calculator/Resources/en-US/Resources.resw @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Calculator + Graphing Calculator {@Appx_ShortDisplayName@}{StringCategory="Feature Title"} This is the title of the official application when published through Windows Store. @@ -1109,9 +1109,9 @@ Delete Text string for the Calculator Delete swipe button in the History list - - Copy - Text string for the Calculator Copy option in the History list context menu + + Copy + Text string for the Calculator Copy option in the History list context menu Delete @@ -1279,7 +1279,7 @@ Equals - Screen reader prompt for the invert button on the scientific operator keypad + Screen reader prompt for the equals button on the scientific operator keypad Inverse Function @@ -3163,10 +3163,6 @@ AB AccessKey for the About button. {StringCategory="Accelerator"} - - 4 - AccessKey for the Date Calculation mode navbar item. {StringCategory="Accelerator"} - I Access key for the History button. {StringCategory="Accelerator"} @@ -3179,18 +3175,6 @@ H Access key for the Hamburger button. {StringCategory="Accelerator"} - - 3 - AccessKey for the Programmer mode navbar item. {StringCategory="Accelerator"} - - - 2 - AccessKey for the Scientific mode navbar item. {StringCategory="Accelerator"} - - - 1 - AccessKey for the Standard mode navbar item. {StringCategory="Accelerator"} - AN AccessKey for the angle converter navbar item. {StringCategory="Accelerator"} @@ -3533,7 +3517,7 @@ Calculation failed - Text displayed when the application is not able to do a calculation + Text displayed when the application is not able to do a calculation Log base X @@ -3551,6 +3535,10 @@ Function Displayed on the button that contains a flyout for the general functions in scientific mode. + + Inequalities + Displayed on the button that contains a flyout for the inequality functions. + Bitwise Displayed on the button that contains a flyout for the bitwise functions in programmer mode. @@ -3851,4 +3839,423 @@ most significant bit Used to describe the last bit of a binary number. Used in bit flip - \ No newline at end of file + + Graphing + Name of the Graphing mode of the Calculator app. Displayed in the navigation menu. + + + = + {Locked}This is the character that should trigger this button. Note that it is a character and not a key, so it does not come from the Windows::System::VirtualKey enum. + + + Equals + Screen reader prompt for the equal button on the graphing calculator operator keypad + + + Enter + {Locked}This is the value from the VirtualKey enum that maps to this button + + + Plot + Screen reader prompt for the plot button on the graphing calculator operator keypad + + + X + {Locked}This is the value that comes from the VirtualKey enum that represents the button. This value is not localized and must be one value that comes from the Windows::System::VirtualKey enum. + + + Y + {Locked}This is the value that comes from the VirtualKey enum that represents the button. This value is not localized and must be one value that comes from the Windows::System::VirtualKey enum. + + + ^ + {Locked}This is the character that should trigger this button. Note that it is a character and not a key, so it does not come from the Windows::System::VirtualKey enum. + + + Home + {Locked}This is the shortcut for the zoom reset button. + + + Reset View + This is the tool tip automation name for the Calculator zoom reset button. + + + Reset View + Screen reader prompt for the reset zoom button. + + + Zoom In + This is the tool tip automation name for the Calculator zoom in button. + + + Zoom In + Screen reader prompt for the zoom in button. + + + Zoom Out + This is the tool tip automation name for the Calculator zoom out button. + + + Zoom Out + Screen reader prompt for the zoom out button. + + + Add Equation + Placeholder text for the equation input button + + + Unable to share at this time. + If there is an error in the sharing action will display a dialog with this text. + + + OK + Used on the dismiss button of the share action error dialog. + + + Look what I graphed with Windows Calculator + Sent as part of the shared content. The title for the share. + + + Equations + Header that appears over the equations section when sharing + + + Variables + Header that appears over the variables section when sharing + + + Image of a graph with equations + Alt text for the graph image when output via Share + + + Variables + Header text for variables area + + + Step + Label text for the step text box + + + Min + Label text for the min text box + + + Max + Label text for the max text box + + + Line Color + Label for the Line Color section of the style picker + + + Key Graph Features + Title for KeyGraphFeatures Control + + + The function does not have any horizontal asymptotes. + Message displayed when the graph does not have any horizontal asymptotes + + + The function does not have any inflection points. + Message displayed when the graph does not have any inflection points + + + The function does not have any maxima points. + Message displayed when the graph does not have any maxima + + + The function does not have any minima points. + Message displayed when the graph does not have any minima + + + Constant + String describing constant monotonicity of a function + + + Decreasing + String describing decreasing monotonicity of a function + + + Unable to determine the monotonicity of the function. + Error displayed when monotonicity cannot be determined + + + Increasing + String describing increasing monotonicity of a function + + + The monotonicity of the function is unknown. + Error displayed when monotonicity is unknown + + + The function does not have any oblique aysmptotes. + Message displayed when the graph does not have any oblique asymptotes + + + Unable to determine the parity of the function. + Error displayed when parity is cannot be determined + + + The function is even. + Message displayed with the function parity is even + + + The function is neither even nor odd. + Message displayed with the function parity is neither even nor odd + + + The function is odd. + Message displayed with the function parity is odd + + + The function parity is unknown. + Error displayed when parity is unknown + + + Periodicity is not supported for this function. + Error displayed when periodicity is not supported + + + The function is not periodic. + Message displayed with the function periodicity is not periodic + + + The function periodicity is unknown. + Message displayed with the function periodicity is unknown + + + These features are too complex for Calculator to calculate: + Error displayed when analysis features cannot be calculated + + + The function does not have any vertical asymptotes. + Message displayed when the graph does not have any vertical asymptotes + + + The function does not have any x-intercepts. + Message displayed when the graph does not have any x-intercepts + + + The function does not have any y-intercepts. + Message displayed when the graph does not have any y-intercepts + + + Domain + Title for KeyGraphFeatures Domain Property + + + Horizontal Asymptotes + Title for KeyGraphFeatures Horizontal Aysmptotes Property + + + Inflection Points + Title for KeyGraphFeatures Inflection Points Property + + + Analysis is not supported for this function. + Error displayed when graph analysis is not supported or had an error. + + + Maxima + Title for KeyGraphFeatures Maxima Property + + + Minima + Title for KeyGraphFeatures Minima Property + + + Monotonicity + Title for KeyGraphFeatures Monotonicity Property + + + Oblique Asymptotes + Title for KeyGraphFeatures Oblique Asymptotes Property + + + Parity + Title for KeyGraphFeatures Parity Property + + + Period + Title for KeyGraphFeatures Periodicity Property + + + Range + Title for KeyGraphFeatures Range Property + + + Vertical Asymptotes + Title for KeyGraphFeatures Vertical Asymptotes Property + + + X-Intercept + Title for KeyGraphFeatures XIntercept Property + + + Y-Intercept + Title for KeyGraphFeatures YIntercept Property + + + Analysis could not be performed for the function. + + + Unable to calculate the domain for this function. + Error displayed when Domain is not returned from the analyzer. + + + Unable to calculate the range for this function. + Error displayed when Range is not returned from the analyzer. + + + Back + This is the tooltip for the back button in the equation analysis page in the graphing calculator + + + Back + This is the automation name for the back button in the equation analysis page in the graphing calculator + + + Analyze equation + This is the tooltip for the analyze equation button + + + Analyze equation + This is the automation name for the analyze equation button + + + Analyze equation + This is the text for the for the analyze equation context menu command + + + Remove equation + This is the tooltip for the graphing calculator remove equation buttons + + + Remove equation + This is the automation name for the graphing calculator remove equation buttons + + + Remove equation + This is the text for the for the remove equation context menu command + + + Share + This is the automation name for the graphing calculator share button. + + + Share + This is the tooltip for the graphing calculator share button. + + + Change equation style + This is the tooltip for the graphing calculator equation style button + + + Change equation style + This is the automation name for the graphing calculator equation style button + + + Change equation style + This is the text for the for the equation style context menu command + + + Show + This is the tooltip/automation name shown when visibility is set to hidden in the graphing calculator + + + Hide + This is the tooltip/automation name shown when visibility is set to visible in the graphing calculator + + + Stop tracing + This is the tooltip/automation name for the graphing calculator stop tracing button + + + Start tracing + This is the tooltip/automation name for the graphing calculator start tracing button + + + Configure slider + This is the tooltip text for the slider options button in Graphing Calculator + + + Configure slider + This is the automation name text for the slider options button in Graphing Calculator + + + Switch to equation mode + Used in Graphing Calculator to switch the view to the equation mode + + + Switch to graph mode + Used in Graphing Calculator to switch the view to the graph mode + + + Switch to equation mode + Used in Graphing Calculator to switch the view to the equation mode + + + Current mode is equation mode + Announcement used in Graphing Calculator when switching to the equation mode + + + Current mode is graph mode + Announcement used in Graphing Calculator when switching to the graph mode + + + Grid + Heading for grid extents on the settings + + + Degrees + Degrees mode on settings page + + + Gradians + Gradian mode on settings page + + + Radians + Radians mode on settings page + + + Units + Heading for Unit's on the settings + + + X-Max + X maximum value header + + + X-Min + X minimum value header + + + Y-Max + Y Maximum value header + + + Y-Min + Y minimum value header + + + Enter an equation + Used in the Graphing Calculator to indicate to users that they can enter an equation in the textbox + + + Grid options + This is the tooltip text for the grid options button in Graphing Calculator + + + Grid options + This is the automation name text for the grid options button in Graphing Calculator + + + Graph Options + Heading for the Graph Options flyout in Graphing mode. + + + Enter an equation + this is the placeholder text used by the textbox to enter an equation + + diff --git a/src/Calculator/TemplateSelectors/KeyGraphFeaturesTemplateSelector.cpp b/src/Calculator/TemplateSelectors/KeyGraphFeaturesTemplateSelector.cpp new file mode 100644 index 000000000..58e0b7eab --- /dev/null +++ b/src/Calculator/TemplateSelectors/KeyGraphFeaturesTemplateSelector.cpp @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "KeyGraphFeaturesTemplateSelector.h" +#include "CalcViewModel/GraphingCalculator/EquationViewModel.h" + +using namespace CalculatorApp::ViewModel; +using namespace CalculatorApp::TemplateSelectors; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +DataTemplate ^ KeyGraphFeaturesTemplateSelector::SelectTemplateCore(Object ^ item) +{ + auto kgfItem = static_cast(item); + + if (!kgfItem->IsText) + { + if (kgfItem->DisplayItems->Size != 0) + { + return RichEditTemplate; + } + else if (kgfItem->GridItems->Size != 0) + { + return GridTemplate; + } + } + + return TextBlockTemplate; +} + +DataTemplate ^ KeyGraphFeaturesTemplateSelector::SelectTemplateCore(Object ^ item, DependencyObject ^ container) +{ + return SelectTemplateCore(item); +} diff --git a/src/Calculator/TemplateSelectors/KeyGraphFeaturesTemplateSelector.h b/src/Calculator/TemplateSelectors/KeyGraphFeaturesTemplateSelector.h new file mode 100644 index 000000000..c1e814841 --- /dev/null +++ b/src/Calculator/TemplateSelectors/KeyGraphFeaturesTemplateSelector.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +namespace CalculatorApp +{ + namespace TemplateSelectors + { + public + ref class KeyGraphFeaturesTemplateSelector sealed : Windows::UI::Xaml::Controls::DataTemplateSelector + { + public: + KeyGraphFeaturesTemplateSelector() + { + } + + property Windows::UI::Xaml::DataTemplate ^ RichEditTemplate; + property Windows::UI::Xaml::DataTemplate ^ GridTemplate; + property Windows::UI::Xaml::DataTemplate ^ TextBlockTemplate; + + Windows::UI::Xaml::DataTemplate ^ SelectTemplateCore(Platform::Object ^ item) override; + Windows::UI::Xaml::DataTemplate ^ SelectTemplateCore(Platform::Object ^ item, Windows::UI::Xaml::DependencyObject ^ container) override; + }; + } +} diff --git a/src/Calculator/Utils/VisualTree.cpp b/src/Calculator/Utils/VisualTree.cpp new file mode 100644 index 000000000..2c1180aaf --- /dev/null +++ b/src/Calculator/Utils/VisualTree.cpp @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "VisualTree.h" + +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; +using namespace Platform; +using namespace Windows::UI::Xaml::Interop; +using namespace Windows::UI::Xaml::Media; +using namespace Calculator::Utils; + +FrameworkElement ^ VisualTree::FindDescendantByName(DependencyObject ^ element, String ^ name) +{ + if (element == nullptr || name == nullptr || name->Length() == 0) + { + return nullptr; + } + + auto frameworkElement = dynamic_cast(element); + auto nsame = frameworkElement->Name->Data(); + nsame = nsame; + if (frameworkElement != nullptr && name->Equals(frameworkElement->Name)) + { + return frameworkElement; + } + + auto childCount = VisualTreeHelper::GetChildrenCount(element); + for (int i = 0; i < childCount; i++) + { + auto result = FindDescendantByName(VisualTreeHelper::GetChild(element, i), name); + if (result != nullptr) + { + return result; + } + } + + return nullptr; +} + +DependencyObject ^ VisualTree::FindDescendant(DependencyObject ^ element, TypeName typeName) +{ + DependencyObject ^ retValue = nullptr; + auto childrenCount = VisualTreeHelper::GetChildrenCount(element); + + for (auto i = 0; i < childrenCount; i++) + { + auto child = VisualTreeHelper::GetChild(element, i); + if (child->GetType() == typeName) + { + retValue = child; + break; + } + + retValue = FindDescendant(child, typeName); + + if (retValue != nullptr) + { + break; + } + } + + return retValue; +} + +FrameworkElement ^ VisualTree::FindAscendantByName(DependencyObject ^ element, String ^ name) +{ + if (element == nullptr || name == nullptr || name->Length() == 0) + { + return nullptr; + } + + auto parent = VisualTreeHelper::GetParent(element); + + if (parent == nullptr) + { + return nullptr; + } + auto frameworkElement = dynamic_cast(parent); + if (frameworkElement != nullptr && name->Equals(frameworkElement->Name)) + { + return frameworkElement; + } + + return FindAscendantByName(parent, name); +} + +Object ^ VisualTree::FindAscendant(DependencyObject ^ element, TypeName typeName) +{ + auto parent = VisualTreeHelper::GetParent(element); + + if (parent == nullptr) + { + return nullptr; + } + + if (parent->GetType() == typeName) + { + return parent; + } + + return FindAscendant(parent, typeName); +} diff --git a/src/Calculator/Utils/VisualTree.h b/src/Calculator/Utils/VisualTree.h new file mode 100644 index 000000000..6ea53194f --- /dev/null +++ b/src/Calculator/Utils/VisualTree.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Light C++/CX port of Microsoft.Toolkit.Uwp.UI.Extensions.VisualTree from the Windows Community toolkit +// Original version here: +// https://raw.githubusercontent.com/windows-toolkit/WindowsCommunityToolkit/master/Microsoft.Toolkit.Uwp.UI/Extensions/Tree/VisualTree.cs + +namespace Calculator::Utils +{ + /// + /// Defines a collection of extensions methods for UI. + /// + ref class VisualTree sealed + { + /// + /// Find descendant control using its name. + /// + /// Parent element. + /// Name of the control to find + /// Descendant control or null if not found. + internal : static Windows::UI::Xaml::FrameworkElement ^ FindDescendantByName(Windows::UI::Xaml::DependencyObject ^ element, Platform::String ^ name); + + /// + /// Find first descendant control of a specified type. + /// + /// Parent element. + /// Type of descendant. + /// Descendant control or null if not found. + static Windows::UI::Xaml::DependencyObject + ^ FindDescendant(Windows::UI::Xaml::DependencyObject ^ element, Windows::UI::Xaml::Interop::TypeName typeName); + + + /// + /// Find visual ascendant control using its name. + /// + /// Parent element. + /// Name of the control to find + /// Descendant control or null if not found. + static Windows::UI::Xaml::FrameworkElement ^ FindAscendantByName(Windows::UI::Xaml::DependencyObject ^ element, Platform::String ^ name); + + /// + /// Find first visual ascendant control of a specified type. + /// + /// Child element. + /// Type of ascendant to look for. + /// Ascendant control or null if not found. + static Platform::Object ^ FindAscendant(Windows::UI::Xaml::DependencyObject ^ element, Windows::UI::Xaml::Interop::TypeName type); + + }; +} diff --git a/src/Calculator/Views/Calculator.xaml.h b/src/Calculator/Views/Calculator.xaml.h index 3b8a6afa0..16979d83a 100644 --- a/src/Calculator/Views/Calculator.xaml.h +++ b/src/Calculator/Views/Calculator.xaml.h @@ -13,7 +13,10 @@ #include "Controls/OverflowTextBlock.h" #include "Controls/OperatorPanelListView.h" #include "Controls/OperatorPanelButton.h" +#include "Controls/EquationTextBox.h" +#include "Controls/MathRichEditBox.h" #include "CalcViewModel/HistoryViewModel.h" +#include "TemplateSelectors/KeyGraphFeaturesTemplateSelector.h" #include "Views/CalculatorProgrammerDisplayPanel.xaml.h" #include "Views/CalculatorProgrammerOperators.xaml.h" #include "Views/CalculatorScientificAngleButtons.xaml.h" diff --git a/src/Calculator/Views/CalculatorProgrammerRadixOperators.xaml b/src/Calculator/Views/CalculatorProgrammerRadixOperators.xaml index 9b0d0bd0c..ce4f5986e 100644 --- a/src/Calculator/Views/CalculatorProgrammerRadixOperators.xaml +++ b/src/Calculator/Views/CalculatorProgrammerRadixOperators.xaml @@ -115,13 +115,8 @@ - - - - - - - + + @@ -172,13 +167,8 @@ - - - - - - - + + @@ -227,13 +217,8 @@ - - - - - - - + + diff --git a/src/Calculator/Views/CalculatorScientificOperators.xaml b/src/Calculator/Views/CalculatorScientificOperators.xaml index 9c688382a..20adf96c4 100644 --- a/src/Calculator/Views/CalculatorScientificOperators.xaml +++ b/src/Calculator/Views/CalculatorScientificOperators.xaml @@ -177,14 +177,8 @@ - - - - - - - - + + @@ -273,13 +267,8 @@ - - - - - - - + + @@ -366,13 +355,8 @@ - - - - - - - + + diff --git a/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml b/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml new file mode 100644 index 000000000..a08399987 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml @@ -0,0 +1,821 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml.cpp b/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml.cpp new file mode 100644 index 000000000..be0181b6b --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml.cpp @@ -0,0 +1,378 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "EquationInputArea.xaml.h" +#include "Utils/VisualTree.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace GraphControl; +using namespace CalculatorApp::ViewModel; +using namespace CalculatorApp::Controls; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::System; +using namespace Windows::UI; +using namespace Windows::UI::ViewManagement; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Input; +using namespace GraphControl; +using namespace Calculator::Utils; + +namespace +{ + inline constexpr auto maxEquationSize = 14; + inline constexpr std::array colorAssignmentMapping = { 0, 3, 7, 10, 1, 4, 8, 11, 2, 5, 9, 12, 6, 13 }; + + StringReference EquationsPropertyName(L"Equations"); +} + +EquationInputArea::EquationInputArea() + : m_lastLineColorIndex{ -1 } + , m_AvailableColors{ ref new Vector() } + , m_accessibilitySettings{ ref new AccessibilitySettings() } + , m_equationToFocus{ nullptr } +{ + m_accessibilitySettings->HighContrastChanged += + ref new TypedEventHandler(this, &EquationInputArea::OnHighContrastChanged); + + ReloadAvailableColors(m_accessibilitySettings->HighContrast); + + InitializeComponent(); +} + +void EquationInputArea::OnPropertyChanged(String ^ propertyName) +{ + if (propertyName == EquationsPropertyName) + { + OnEquationsPropertyChanged(); + } +} + +void EquationInputArea::OnEquationsPropertyChanged() +{ + if (Equations != nullptr && Equations->Size == 0) + { + AddNewEquation(); + } +} + +void EquationInputArea::AddNewEquation() +{ + if (Equations->Size > 0) + { + Equations->GetAt(Equations->Size - 1)->IsLastItemInList = false; + } + + // Cap equations at 14 + if (Equations->Size >= maxEquationSize) + { + return; + } + + + m_lastLineColorIndex = (m_lastLineColorIndex + 1) % AvailableColors->Size; + + int colorIndex; + + if (m_accessibilitySettings->HighContrast) + { + colorIndex = m_lastLineColorIndex; + } + else + { + colorIndex = colorAssignmentMapping[m_lastLineColorIndex]; + } + + auto eq = ref new EquationViewModel(ref new Equation(), ++m_lastFunctionLabelIndex, AvailableColors->GetAt(colorIndex)->Color); + eq->IsLastItemInList = true; + m_equationToFocus = eq; + Equations->Append(eq); +} + +void EquationInputArea::EquationTextBox_GotFocus(Object ^ sender, RoutedEventArgs ^ e) +{ + KeyboardShortcutManager::HonorShortcuts(false); +} + +void EquationInputArea::EquationTextBox_LostFocus(Object ^ sender, RoutedEventArgs ^ e) +{ + KeyboardShortcutManager::HonorShortcuts(true); +} + +void EquationInputArea::EquationTextBox_Submitted(Object ^ sender, MathRichEditBoxSubmission ^ submission) +{ + auto tb = static_cast(sender); + if (tb == nullptr) + { + return; + } + auto eq = static_cast(tb->DataContext); + if (eq == nullptr) + { + return; + } + + if (submission->Source == EquationSubmissionSource::ENTER_KEY + || (submission->Source == EquationSubmissionSource::FOCUS_LOST && submission->HasTextChanged && eq->Expression != nullptr + && eq->Expression->Length() > 0)) + { + unsigned int index = 0; + if (Equations->IndexOf(eq, &index)) + { + if (index == Equations->Size - 1) + { + // If it's the last equation of the list + AddNewEquation(); + } + else + { + if (submission->Source == EquationSubmissionSource::ENTER_KEY) + { + auto nextEquation = Equations->GetAt(index + 1); + FocusEquationTextBox(nextEquation); + } + } + } + } +} + +void EquationInputArea::FocusEquationTextBox(EquationViewModel ^ equation) +{ + unsigned int index; + if (!Equations->IndexOf(equation, &index) || index < 0) + { + return; + } + auto container = EquationInputList->TryGetElement(index); + if (container == nullptr) + { + return; + } + auto equationTextBox = dynamic_cast(container); + if (equationTextBox != nullptr) + { + equationTextBox->FocusTextBox(); + } + else + { + auto equationInput = VisualTree::FindDescendantByName(container, "EquationInputButton"); + if (equationInput == nullptr) + { + return; + } + equationTextBox = dynamic_cast(equationInput); + if (equationTextBox != nullptr) + { + equationTextBox->FocusTextBox(); + } + } +} + +void EquationInputArea::EquationTextBox_RemoveButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + auto tb = static_cast(sender); + auto eq = static_cast(tb->DataContext); + unsigned int index; + if (Equations->IndexOf(eq, &index)) + { + if (eq->FunctionLabelIndex == m_lastFunctionLabelIndex) + { + m_lastFunctionLabelIndex--; + } + + if (index == Equations->Size - 1 && Equations->Size > 1) + { + Equations->GetAt(Equations->Size - 2)->IsLastItemInList = true; + } + Equations->RemoveAt(index); + } +} + +void EquationInputArea::EquationTextBox_KeyGraphFeaturesButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + auto tb = static_cast(sender); + auto eq = static_cast(tb->DataContext); + KeyGraphFeaturesRequested(this, eq); +} + +void EquationInputArea::EquationTextBox_EquationButtonClicked(Object ^ sender, RoutedEventArgs ^ e) +{ + auto tb = static_cast(sender); + auto eq = static_cast(tb->DataContext); + eq->IsLineEnabled = !eq->IsLineEnabled; +} + +void EquationInputArea::EquationTextBox_Loaded(Object ^ sender, RoutedEventArgs ^ e) +{ + auto tb = static_cast(sender); + + auto colorChooser = static_cast(tb->ColorChooserFlyout->Content); + colorChooser->AvailableColors = AvailableColors; + + if (m_equationToFocus != nullptr && tb->DataContext == m_equationToFocus) + { + auto copyEquationToFocus = m_equationToFocus; + m_equationToFocus = nullptr; + tb->FocusTextBox(); + + unsigned int index; + if (Equations->IndexOf(copyEquationToFocus, &index)) + { + auto container = EquationInputList->TryGetElement(index); + if (container != nullptr) + { + container->StartBringIntoView(); + } + } + } +} + +void EquationInputArea::EquationTextBox_DataContextChanged(Windows::UI::Xaml::FrameworkElement ^ sender, Windows::UI::Xaml::DataContextChangedEventArgs ^ args) +{ + auto tb = static_cast(sender); + if (!tb->IsLoaded) + { + return; + } + + FocusEquationIfNecessary(tb); +} + +void EquationInputArea::FocusEquationIfNecessary(CalculatorApp::Controls::EquationTextBox ^ textBox) +{ + if (m_equationToFocus != nullptr && textBox->DataContext == m_equationToFocus) + { + m_equationToFocus = nullptr; + textBox->FocusTextBox(); + + unsigned int index; + if (Equations->IndexOf(m_equationToFocus, &index)) + { + auto container = EquationInputList->TryGetElement(index); + if (container != nullptr) + { + container->StartBringIntoView(); + } + } + } +} + +void EquationInputArea::OnHighContrastChanged(AccessibilitySettings ^ sender, Object ^ args) +{ + ReloadAvailableColors(sender->HighContrast); +} + +void EquationInputArea::ReloadAvailableColors(bool isHighContrast) +{ + m_AvailableColors->Clear(); + + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush1"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush2"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush3"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush4"))); + + // If this is not high contrast, we have all 16 colors, otherwise we will restrict this to a subset of high contrast colors + if (!isHighContrast) + { + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush5"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush6"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush7"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush8"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush9"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush10"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush11"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush12"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush13"))); + m_AvailableColors->Append(safe_cast(Application::Current->Resources->Lookup(L"EquationBrush14"))); + } + + // If there are no equations to reload, quit early + if (Equations == nullptr || Equations->Size == 0) + { + return; + } + + // Reassign colors for each equation + m_lastLineColorIndex = -1; + for (auto equationViewModel : Equations) + { + m_lastLineColorIndex = (m_lastLineColorIndex + 1) % AvailableColors->Size; + equationViewModel->LineColor = AvailableColors->GetAt(m_lastLineColorIndex)->Color; + } +} + +void EquationInputArea::TextBoxGotFocus(TextBox ^ sender, RoutedEventArgs ^ e) +{ + sender->SelectAll(); +} + +void EquationInputArea::SubmitTextbox(TextBox ^ sender) +{ + auto variableViewModel = static_cast(sender->DataContext); + double val; + if (sender->Name == "ValueTextBox") + { + val = validateDouble(sender->Text, variableViewModel->Value); + variableViewModel->Value = val; + } + else if (sender->Name == "MinTextBox") + { + val = validateDouble(sender->Text, variableViewModel->Min); + variableViewModel->Min = val; + } + else if (sender->Name == "MaxTextBox") + { + val = validateDouble(sender->Text, variableViewModel->Max); + variableViewModel->Max = val; + } + else if (sender->Name == "StepTextBox") + { + val = validateDouble(sender->Text, variableViewModel->Step); + variableViewModel->Step = val; + } + else + { + return; + } + + wostringstream oss; + oss << std::noshowpoint << val; + sender->Text = ref new String(oss.str().c_str()); +} + +void EquationInputArea::TextBoxLosingFocus(TextBox ^ sender, LosingFocusEventArgs ^) +{ + SubmitTextbox(sender); +} + +void EquationInputArea::TextBoxKeyDown(TextBox ^ sender, KeyRoutedEventArgs ^ e) +{ + if (e->Key == ::VirtualKey::Enter) + { + SubmitTextbox(sender); + } +} + +double EquationInputArea::validateDouble(String ^ value, double defaultValue) +{ + try + { + return stod(value->Data()); + } + catch (...) + { + return defaultValue; + } +} + +::Visibility EquationInputArea::ManageEditVariablesButtonVisibility(unsigned int numberOfVariables) +{ + return numberOfVariables == 0 ? ::Visibility::Collapsed : ::Visibility::Visible; +} diff --git a/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml.h b/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml.h new file mode 100644 index 000000000..17b5889d7 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/EquationInputArea.xaml.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Views/GraphingCalculator/EquationInputArea.g.h" +#include "CalcViewModel/Common/Utils.h" +#include "CalcViewModel/GraphingCalculator/EquationViewModel.h" +#include "CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.h" +#include "EquationStylePanelControl.xaml.h" +#include "Common/KeyboardShortcutManager.h" +#include "Controls/EquationTextBox.h" +#include "Converters/BooleanNegationConverter.h" +#include "Controls/MathRichEditBox.h" + +namespace CalculatorApp +{ + public ref class EquationInputArea sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + EquationInputArea(); + + OBSERVABLE_OBJECT_CALLBACK(OnPropertyChanged); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IObservableVector ^, Equations); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IObservableVector ^, Variables); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IObservableVector ^, AvailableColors); + event Windows::Foundation::EventHandler^ KeyGraphFeaturesRequested; + + public: + static Windows::UI::Xaml::Visibility ManageEditVariablesButtonVisibility(unsigned int numberOfVariables); + + static Windows::UI::Xaml::Media::SolidColorBrush + ^ ToSolidColorBrush(Windows::UI::Color color) { return ref new Windows::UI::Xaml::Media::SolidColorBrush(color); } + + private: + void OnPropertyChanged(Platform::String^ propertyName); + void OnEquationsPropertyChanged(); + + void AddNewEquation(); + + void EquationTextBox_GotFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void EquationTextBox_LostFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void EquationTextBox_Submitted(Platform::Object ^ sender, CalculatorApp::Controls::MathRichEditBoxSubmission ^ e); + + void OnHighContrastChanged(Windows::UI::ViewManagement::AccessibilitySettings ^ sender, Platform::Object ^ args); + void ReloadAvailableColors(bool isHighContrast); + void FocusEquationTextBox(ViewModel::EquationViewModel ^ equation); + + void EquationTextBox_RemoveButtonClicked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void EquationTextBox_KeyGraphFeaturesButtonClicked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void EquationTextBox_EquationButtonClicked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void EquationTextBox_Loaded(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void EquationTextBox_DataContextChanged(Windows::UI::Xaml::FrameworkElement ^ sender, Windows::UI::Xaml::DataContextChangedEventArgs ^ args); + void FocusEquationIfNecessary(_In_ CalculatorApp::Controls::EquationTextBox ^ textBox); + + double validateDouble(Platform::String ^ value, double defaultValue); + void TextBoxGotFocus(Windows::UI::Xaml::Controls::TextBox ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void TextBoxLosingFocus(Windows::UI::Xaml::Controls::TextBox ^ textbox, Windows::UI::Xaml::Input::LosingFocusEventArgs ^ args); + void TextBoxKeyDown(Windows::UI::Xaml::Controls::TextBox ^ textbox, Windows::UI::Xaml::Input::KeyRoutedEventArgs ^ e); + void SubmitTextbox(Windows::UI::Xaml::Controls::TextBox ^ textbox); + + Windows::UI::ViewManagement::AccessibilitySettings ^ m_accessibilitySettings; + int m_lastLineColorIndex; + int m_lastFunctionLabelIndex; + ViewModel::EquationViewModel ^ m_equationToFocus; + }; +} diff --git a/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml b/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml new file mode 100644 index 000000000..0e0f1136e --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml @@ -0,0 +1,553 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4,4,0,0 + 0,0,4,4 + 4,0,0,4 + 0,4,4,0 + + + + 4,4,0,0 + 0,0,4,4 + 4,0,0,4 + 0,4,4,0 + + + + 0 + 0 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml.cpp b/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml.cpp new file mode 100644 index 000000000..83a4ad9c4 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml.cpp @@ -0,0 +1,545 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "GraphingCalculator.xaml.h" +#include "CalcViewModel/Common/AppResourceProvider.h" +#include "CalcViewModel/Common/TraceLogger.h" +#include "CalcViewModel/Common/LocalizationSettings.h" +#include "CalcViewModel/Common/LocalizationStringUtil.h" +#include "Common/KeyboardShortcutManager.h" +#include "CalcViewModel/Common/Automation/NarratorAnnouncement.h" +#include "CalcViewModel/Common/Automation/NarratorNotifier.h" +#include "Controls/CalculationResult.h" +#include "CalcManager/NumberFormattingUtils.h" +#include "Calculator/Controls/EquationTextBox.h" +#include "Calculator/Views/GraphingCalculator/EquationInputArea.xaml.h" +#include "CalcViewModel/Common/Utils.h" +#include "GraphingSettings.xaml.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::Automation; +using namespace CalculatorApp::Controls; +using namespace CalculatorApp::ViewModel; +using namespace CalcManager::NumberFormattingUtils; +using namespace concurrency; +using namespace GraphControl; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace std::chrono; +using namespace Utils; +using namespace Windows::ApplicationModel::DataTransfer; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Storage::Streams; +using namespace Windows::System; +using namespace Windows::UI::Core; +using namespace Windows::UI::Input; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Automation::Peers; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Imaging; +using namespace Windows::UI::Popups; + +constexpr auto sc_ViewModelPropertyName = L"ViewModel"; + +DEPENDENCY_PROPERTY_INITIALIZATION(GraphingCalculator, IsSmallState); + +GraphingCalculator::GraphingCalculator() +{ + InitializeComponent(); + + DataTransferManager ^ dataTransferManager = DataTransferManager::GetForCurrentView(); + + // Register the current control as a share source. + m_dataRequestedToken = dataTransferManager->DataRequested += + ref new TypedEventHandler(this, &GraphingCalculator::OnDataRequested); + + // Request notifications when we should be showing the trace values + GraphingControl->TracingChangedEvent += ref new TracingChangedEventHandler(this, &GraphingCalculator::OnShowTracePopupChanged); + + // And when the actual trace value changes + GraphingControl->TracingValueChangedEvent += ref new TracingValueChangedEventHandler(this, &GraphingCalculator::OnTracePointChanged); + + // Update where the pointer value is (ie: where the user cursor from keyboard inputs moves the point to) + GraphingControl->PointerValueChangedEvent += ref new PointerValueChangedEventHandler(this, &GraphingCalculator::OnPointerPointChanged); + + // OemMinus and OemAdd aren't declared in the VirtualKey enum, we can't add this accelerator XAML-side + auto virtualKey = ref new KeyboardAccelerator(); + virtualKey->Key = (VirtualKey)189; // OemPlus key + virtualKey->Modifiers = VirtualKeyModifiers::Control; + ZoomOutButton->KeyboardAccelerators->Append(virtualKey); + + virtualKey = ref new KeyboardAccelerator(); + virtualKey->Key = (VirtualKey)187; // OemAdd key + virtualKey->Modifiers = VirtualKeyModifiers::Control; + ZoomInButton->KeyboardAccelerators->Append(virtualKey); +} + +void GraphingCalculator::OnShowTracePopupChanged(bool newValue) +{ + if ((TraceValuePopup->Visibility == ::Visibility::Visible) != newValue) + { + TraceValuePopup->Visibility = newValue ? ::Visibility::Visible : ::Visibility::Collapsed; + } +} + +void GraphingCalculator::GraphingCalculator_DataContextChanged(FrameworkElement ^ sender, DataContextChangedEventArgs ^ args) +{ + if (ViewModel != nullptr) + { + if (m_vectorChangedToken.Value != 0) + { + ViewModel->Equations->VectorChanged -= m_vectorChangedToken; + m_vectorChangedToken.Value = 0; + } + + if (m_variableUpdatedToken.Value != 0) + { + ViewModel->VariableUpdated -= m_variableUpdatedToken; + m_variableUpdatedToken.Value = 0; + } + } + + ViewModel = dynamic_cast(args->NewValue); + + m_vectorChangedToken = ViewModel->Equations->VectorChanged += + ref new VectorChangedEventHandler(this, &GraphingCalculator::OnEquationsVectorChanged); + + m_variableUpdatedToken = ViewModel->VariableUpdated += + ref new EventHandler(this, &CalculatorApp::GraphingCalculator::OnVariableChanged); +} + +void GraphingCalculator::OnEquationsVectorChanged(IObservableVector ^ sender, IVectorChangedEventArgs ^ event) +{ + // If an item is already added to the graph, changing it should automatically trigger a graph update + if (event->CollectionChange == ::CollectionChange::ItemChanged) + { + return; + } + + // Do not plot the graph if we are removing an empty equation, just remove it + if (event->CollectionChange == ::CollectionChange::ItemRemoved) + { + auto itemToRemove = GraphingControl->Equations->GetAt(event->Index); + + if (itemToRemove->Expression->IsEmpty()) + { + GraphingControl->Equations->RemoveAt(event->Index); + + return; + } + } + + // Do not plot the graph if we are adding an empty equation, just add it + if (event->CollectionChange == ::CollectionChange::ItemInserted) + { + auto itemToAdd = sender->GetAt(event->Index); + + if (itemToAdd->Expression->IsEmpty()) + { + GraphingControl->Equations->Append(itemToAdd->GraphEquation); + + return; + } + } + + // We are either adding or removing a valid equation, or resetting the collection. We will need to plot the graph + GraphingControl->Equations->Clear(); + + for (auto equationViewModel : ViewModel->Equations) + { + GraphingControl->Equations->Append(equationViewModel->GraphEquation); + } + + GraphingControl->PlotGraph(false); +} + +void GraphingCalculator::OnTracePointChanged(Point newPoint) +{ + wstringstream traceValueString; + + // TODO: The below precision should ideally be dynamic based on the current scale of the graph. + traceValueString << "(" << fixed << setprecision(1) << newPoint.X << ", " << fixed << setprecision(1) << newPoint.Y << ")"; + + TraceValue->Text = ref new String(traceValueString.str().c_str()); + + auto peer = FrameworkElementAutomationPeer::FromElement(TraceValue); + + if (peer != nullptr) + { + peer->RaiseAutomationEvent(AutomationEvents::LiveRegionChanged); + } + + PositionGraphPopup(); +} + +void CalculatorApp::GraphingCalculator::OnPointerPointChanged(Windows::Foundation::Point newPoint) +{ + // Move the pointer glyph to where it is supposed to be. + // because the glyph is centered and has some spacing, to get the point to properly line up with the glyph, move the x point over 2 px + TracePointer->Margin = Thickness(newPoint.X - 2, newPoint.Y, 0, 0); +} + +GraphingCalculatorViewModel ^ GraphingCalculator::ViewModel::get() +{ + return m_viewModel; +} + +void GraphingCalculator::ViewModel::set(GraphingCalculatorViewModel ^ vm) +{ + if (m_viewModel != vm) + { + m_viewModel = vm; + RaisePropertyChanged(StringReference(sc_ViewModelPropertyName)); + } +} + +void CalculatorApp::GraphingCalculator::OnShareClick(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) +{ + // Ask the OS to start a share action. + DataTransferManager::ShowShareUI(); +} + +// When share is invoked (by the user or programmatically) the event handler we registered will be called to populate the data package with the +// data to be shared. We will request the current graph image from the grapher as a stream that will pass to the share request. +void GraphingCalculator::OnDataRequested(DataTransferManager ^ sender, DataRequestedEventArgs ^ args) +{ + auto resourceLoader = ResourceLoader::GetForCurrentView(); + + try + { + std::wstringstream rawHtml; + std::wstringstream equationHtml; + + rawHtml << L""; + + auto equations = ViewModel->Equations; + bool hasEquations = false; + + if (equations->Size > 0) + { + equationHtml << L""; + equationHtml << resourceLoader->GetString(L"EquationsShareHeader")->Data(); + equationHtml << L""; + equationHtml << L""; + + for (auto equation : equations) + { + auto expression = equation->Expression; + if (expression->IsEmpty()) + { + continue; + } + + auto color = equation->LineColor; + hasEquations = true; + + expression = GraphingControl->ConvertToLinear(expression); + + std::wstringstream equationColorHtml; + equationColorHtml << L"color:rgb(" << color.R.ToString()->Data() << L"," << color.G.ToString()->Data() << L"," << color.B.ToString()->Data() + << L");"; + + equationHtml << L"■"; + equationHtml << EscapeHtmlSpecialCharacters(expression)->Data(); + equationHtml << L""; + } + equationHtml << L""; + } + + if (hasEquations) + { + rawHtml << equationHtml.str(); + } + + auto variables = ViewModel->Variables; + + if (variables->Size > 0) + { + auto localizedSeperator = LocalizationSettings::GetInstance().GetListSeparator() + L" "; + + rawHtml << L""; + rawHtml << resourceLoader->GetString(L"VariablesShareHeader")->Data(); + rawHtml << L""; + + for (unsigned i = 0; i < variables->Size; i++) + { + auto name = variables->GetAt(i)->Name; + auto value = variables->GetAt(i)->Value; + + rawHtml << name->Data(); + rawHtml << L"="; + auto formattedValue = to_wstring(value); + TrimTrailingZeros(formattedValue); + rawHtml << formattedValue; + + if (variables->Size - 1 != i) + { + rawHtml << localizedSeperator; + } + } + + rawHtml << L""; + } + + rawHtml << L""; + + // Shortcut to the request data + auto requestData = args->Request->Data; + + DataPackage ^ dataPackage = ref new DataPackage(); + auto html = HtmlFormatHelper::CreateHtmlFormat(ref new String(rawHtml.str().c_str())); + + requestData->Properties->Title = resourceLoader->GetString(L"ShareActionTitle"); + + requestData->SetHtmlFormat(html); + + auto bitmapStream = GraphingControl->GetGraphBitmapStream(); + + requestData->ResourceMap->Insert(ref new String(L"graph.png"), bitmapStream); + + // Set the thumbnail image (in case the share target can't handle HTML) + requestData->Properties->Thumbnail = bitmapStream; + } + catch (Exception ^ ex) + { + TraceLogger::GetInstance()->LogPlatformException(ViewMode::Graphing, __FUNCTIONW__, ex); + + // Something went wrong, notify the user. + + auto errDialog = ref new ContentDialog(); + errDialog->Content = resourceLoader->GetString(L"ShareActionErrorMessage"); + errDialog->CloseButtonText = resourceLoader->GetString(L"ShareActionErrorOk"); + errDialog->ShowAsync(); + } +} + +void GraphingCalculator::GraphingControl_VariablesUpdated(Object ^, Object ^) +{ + m_viewModel->UpdateVariables(GraphingControl->Variables); +} + +void GraphingCalculator::OnVariableChanged(Platform::Object ^ sender, VariableChangedEventArgs args) +{ + GraphingControl->SetVariable(args.variableName, args.newValue); +} + +void GraphingCalculator::OnZoomInCommand(Object ^ /* parameter */) +{ + GraphingControl->ZoomFromCenter(zoomInScale); +} + +void GraphingCalculator::OnZoomOutCommand(Object ^ /* parameter */) +{ + GraphingControl->ZoomFromCenter(zoomOutScale); +} + +void GraphingCalculator::OnZoomResetCommand(Object ^ /* parameter */) +{ + GraphingControl->ResetGrid(); +} + +String ^ GraphingCalculator::GetTracingLegend(Platform::IBox ^ isTracing) +{ + auto resProvider = AppResourceProvider::GetInstance(); + return isTracing != nullptr && isTracing->Value ? resProvider->GetResourceString(L"disableTracingButtonToolTip") + : resProvider->GetResourceString(L"enableTracingButtonToolTip"); +} + +void GraphingCalculator::GraphingControl_LostFocus(Object ^ sender, RoutedEventArgs ^ e) +{ + // If the graph is losing focus while we are in active tracing we need to turn it off so we don't try to eat keys in other controls. + if (GraphingControl->ActiveTracing) + { + if (ActiveTracing->Equals(FocusManager::GetFocusedElement()) && ActiveTracing->IsPressed) + { + m_ActiveTracingPointerCaptureLost = ActiveTracing->PointerCaptureLost += + ref new Windows::UI::Xaml::Input::PointerEventHandler(this, &CalculatorApp::GraphingCalculator::ActiveTracing_PointerCaptureLost); + } + else + { + GraphingControl->ActiveTracing = false; + OnShowTracePopupChanged(false); + } + } +} + +void CalculatorApp::GraphingCalculator::ActiveTracing_PointerCaptureLost(Platform::Object ^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) +{ + if (m_ActiveTracingPointerCaptureLost.Value != 0) + { + ActiveTracing->PointerCaptureLost -= m_ActiveTracingPointerCaptureLost; + m_ActiveTracingPointerCaptureLost.Value = 0; + } + + if (GraphingControl->ActiveTracing) + { + GraphingControl->ActiveTracing = false; + OnShowTracePopupChanged(false); + } +} + +void GraphingCalculator::GraphingControl_LosingFocus(UIElement ^ sender, LosingFocusEventArgs ^ args) +{ + auto newFocusElement = dynamic_cast(args->NewFocusedElement); + if (newFocusElement == nullptr || newFocusElement->Name == nullptr) + { + // Because clicking on the swap chain panel will try to move focus to a control that can't actually take focus + // we will get a null destination. So we are going to try and cancel that request. + // If the destination is not in our application we will also get a null destination but the cancel will fail so it doesn't hurt to try. + args->TryCancel(); + } +} + +void GraphingCalculator::OnEquationKeyGraphFeaturesRequested(Object ^ sender, EquationViewModel ^ equationViewModel) +{ + ViewModel->SetSelectedEquation(equationViewModel); + if (equationViewModel != nullptr) + { + auto keyGraphFeatureInfo = GraphingControl->AnalyzeEquation(equationViewModel->GraphEquation); + equationViewModel->PopulateKeyGraphFeatures(keyGraphFeatureInfo); + IsKeyGraphFeaturesVisible = true; + } +} + +void GraphingCalculator::OnKeyGraphFeaturesClosed(Object ^ sender, RoutedEventArgs ^ e) +{ + IsKeyGraphFeaturesVisible = false; +} + +Visibility GraphingCalculator::ShouldDisplayPanel(bool isSmallState, bool isEquationModeActivated, bool isGraphPanel) +{ + return (!isSmallState || isEquationModeActivated ^ isGraphPanel) ? ::Visibility::Visible : ::Visibility::Collapsed; +} + +Platform::String ^ GraphingCalculator::GetInfoForSwitchModeToggleButton(bool isChecked) +{ + if (isChecked) + { + return AppResourceProvider::GetInstance()->GetResourceString(L"GraphSwitchToGraphMode"); + } + else + { + return AppResourceProvider::GetInstance()->GetResourceString(L"GraphSwitchToEquationMode"); + } +} + +void GraphingCalculator::SwitchModeToggleButton_Checked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) +{ + auto narratorNotifier = ref new NarratorNotifier(); + String ^ announcementText; + if (SwitchModeToggleButton->IsChecked->Value) + { + announcementText = AppResourceProvider::GetInstance()->GetResourceString(L"GraphSwitchedToEquationModeAnnouncement"); + } + else + { + announcementText = AppResourceProvider::GetInstance()->GetResourceString(L"GraphSwitchedToGraphModeAnnouncement"); + } + + auto announcement = CalculatorAnnouncement::GetGraphModeChangedAnnouncement(announcementText); + narratorNotifier->Announce(announcement); +} + +void GraphingCalculator::PositionGraphPopup() +{ + if (GraphingControl->TraceLocation.X + 15 + TraceValuePopup->ActualWidth >= GraphingControl->ActualWidth) + { + TraceValuePopupTransform->X = (int)GraphingControl->TraceLocation.X - 15 - TraceValuePopup->ActualWidth; + } + else + { + TraceValuePopupTransform->X = (int)GraphingControl->TraceLocation.X + 15; + } + + if (GraphingControl->TraceLocation.Y >= 30) + { + TraceValuePopupTransform->Y = (int)GraphingControl->TraceLocation.Y - 30; + } + else + { + TraceValuePopupTransform->Y = (int)GraphingControl->TraceLocation.Y; + } +} + +void GraphingCalculator::TraceValuePopup_SizeChanged(Object ^ sender, SizeChangedEventArgs ^ e) +{ + PositionGraphPopup(); +} + +::Visibility GraphingCalculator::ManageEditVariablesButtonVisibility(unsigned int numberOfVariables) +{ + return numberOfVariables == 0 ? ::Visibility::Collapsed : ::Visibility::Visible; +} + +void CalculatorApp::GraphingCalculator::ActiveTracing_Checked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) +{ + FocusManager::TryFocusAsync(GraphingControl, ::FocusState::Programmatic); + + m_activeTracingKeyUpToken = Window::Current->CoreWindow->KeyUp += + ref new TypedEventHandler( + this, &CalculatorApp::GraphingCalculator::ActiveTracing_KeyUp); + + KeyboardShortcutManager::IgnoreEscape(false); + + TracePointer->Visibility = ::Visibility::Visible; +} + +void CalculatorApp::GraphingCalculator::ActiveTracing_Unchecked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) +{ + if (m_ActiveTracingPointerCaptureLost.Value != 0) + { + ActiveTracing->PointerCaptureLost -= m_ActiveTracingPointerCaptureLost; + m_ActiveTracingPointerCaptureLost.Value = 0; + } + + if (m_activeTracingKeyUpToken.Value != 0) + { + Window::Current->CoreWindow->KeyUp -= m_activeTracingKeyUpToken; + m_activeTracingKeyUpToken.Value = 0; + } + KeyboardShortcutManager::HonorEscape(); + + TracePointer->Visibility = ::Visibility::Collapsed; +} + +void CalculatorApp::GraphingCalculator::ActiveTracing_KeyUp(Windows::UI::Core::CoreWindow ^ sender, Windows::UI::Core::KeyEventArgs ^ args) +{ + if (args->VirtualKey == VirtualKey::Escape) + { + GraphingControl->ActiveTracing = false; + args->Handled = true; + } +} + +void GraphingCalculator::GraphSettingsButton_Click(Object ^ sender, RoutedEventArgs ^ e) +{ + DisplayGraphSettings(); +} + +void GraphingCalculator::DisplayGraphSettings() +{ + auto graphSettings = ref new GraphingSettings(); + graphSettings->SetGrapher(this->GraphingControl); + auto flyoutGraphSettings = ref new Flyout(); + flyoutGraphSettings->Content = graphSettings; + flyoutGraphSettings->Closing += ref new TypedEventHandler(this, &GraphingCalculator::OnSettingsFlyout_Closing); + flyoutGraphSettings->ShowAt(GraphSettingsButton); +} + +void GraphingCalculator::OnSettingsFlyout_Closing(FlyoutBase ^ sender, FlyoutBaseClosingEventArgs ^ args) +{ + auto flyout = static_cast(sender); + auto graphingSetting = static_cast(flyout->Content); + args->Cancel = graphingSetting->CanBeClose(); +} diff --git a/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml.h b/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml.h new file mode 100644 index 000000000..f5dd11a4b --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingCalculator.xaml.h @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Views\GraphingCalculator\GraphingCalculator.g.h" +#include "CalcViewModel\GraphingCalculator\GraphingCalculatorViewModel.h" +#include "Views\NumberPad.xaml.h" +#include "Views\GraphingCalculator\KeyGraphFeaturesPanel.xaml.h" +#include "Views\GraphingCalculator\GraphingNumPad.xaml.h" +#include "Views\GraphingCalculator\GraphingSettings.xaml.h" + +namespace CalculatorApp +{ + constexpr double zoomInScale = 1 / 1.0625; + constexpr double zoomOutScale = 1.0625; + +public ref class GraphingCalculator sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + GraphingCalculator(); + + OBSERVABLE_OBJECT(); + DEPENDENCY_PROPERTY_OWNER(GraphingCalculator); + COMMAND_FOR_METHOD(ZoomOutButtonPressed, GraphingCalculator::OnZoomOutCommand); + COMMAND_FOR_METHOD(ZoomInButtonPressed, GraphingCalculator::OnZoomInCommand); + COMMAND_FOR_METHOD(ZoomResetButtonPressed, GraphingCalculator::OnZoomResetCommand); + OBSERVABLE_PROPERTY_R(bool, IsKeyGraphFeaturesVisible); + DEPENDENCY_PROPERTY(bool, IsSmallState); + + property CalculatorApp::ViewModel::GraphingCalculatorViewModel^ ViewModel + { + CalculatorApp::ViewModel::GraphingCalculatorViewModel^ get(); + void set(CalculatorApp::ViewModel::GraphingCalculatorViewModel^ vm); + } + + static Windows::UI::Xaml::Visibility ShouldDisplayPanel(bool isSmallState, bool isEquationModeActivated, bool isGraphPanel); + static Platform::String ^ GetInfoForSwitchModeToggleButton(bool isChecked); + static Windows::UI::Xaml::Visibility ManageEditVariablesButtonVisibility(unsigned int numberOfVariables); + static Platform::String ^ GetTracingLegend(Platform::IBox ^ isTracing); + private: + void GraphingCalculator_DataContextChanged(Windows::UI::Xaml::FrameworkElement ^ sender, Windows::UI::Xaml::DataContextChangedEventArgs ^ args); + + void OnVariableChanged(Platform::Object ^ sender, CalculatorApp::ViewModel::VariableChangedEventArgs args); + void OnEquationsVectorChanged( + Windows::Foundation::Collections::IObservableVector ^ sender, + Windows::Foundation::Collections::IVectorChangedEventArgs ^ event); + + void OnZoomInCommand(Object ^ parameter); + void OnZoomOutCommand(Object ^ parameter); + void OnZoomResetCommand(Object ^ parameter); + + double validateDouble(Platform::String ^ value, double defaultValue); + + void OnShareClick(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + + void OnShowTracePopupChanged(bool newValue); + void OnTracePointChanged(Windows::Foundation::Point newPoint); + void OnPointerPointChanged(Windows::Foundation::Point newPoint); + private: + void OnDataRequested( + Windows::ApplicationModel::DataTransfer::DataTransferManager ^ sender, + Windows::ApplicationModel::DataTransfer::DataRequestedEventArgs ^ e); + + void TextBoxGotFocus(Windows::UI::Xaml::Controls::TextBox ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void GraphingControl_LostFocus(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void GraphingControl_LosingFocus(Windows::UI::Xaml::UIElement ^ sender, Windows::UI::Xaml::Input::LosingFocusEventArgs ^ args); + void GraphingControl_VariablesUpdated(Platform::Object ^ sender, Object ^ args); + void OnEquationKeyGraphFeaturesRequested(Platform::Object ^ sender, CalculatorApp::ViewModel::EquationViewModel ^ e); + void OnKeyGraphFeaturesClosed(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void SwitchModeToggleButton_Checked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void TraceValuePopup_SizeChanged(Platform::Object ^ sender, Windows::UI::Xaml::SizeChangedEventArgs ^ e); + void PositionGraphPopup(); + void ActiveTracing_Checked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void ActiveTracing_Unchecked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void ActiveTracing_KeyUp(Windows::UI::Core::CoreWindow ^ sender, Windows::UI::Core::KeyEventArgs ^ args); + void ActiveTracing_PointerCaptureLost(Platform::Object ^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e); + void GraphSettingsButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + + void DisplayGraphSettings(); + + private: + Windows::Foundation::EventRegistrationToken m_dataRequestedToken; + Windows::Foundation::EventRegistrationToken m_vectorChangedToken; + Windows::Foundation::EventRegistrationToken m_variableUpdatedToken; + Windows::Foundation::EventRegistrationToken m_activeTracingKeyUpToken; + Windows::Foundation::EventRegistrationToken m_ActiveTracingPointerCaptureLost; + CalculatorApp::ViewModel::GraphingCalculatorViewModel ^ m_viewModel; + void + OnSettingsFlyout_Closing(Windows::UI::Xaml::Controls::Primitives::FlyoutBase ^ sender, Windows::UI::Xaml::Controls::Primitives::FlyoutBaseClosingEventArgs ^ args); + }; + +} diff --git a/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml b/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml new file mode 100644 index 000000000..ded7bbc41 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml @@ -0,0 +1,1315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml.cpp b/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml.cpp new file mode 100644 index 000000000..751e8a5a5 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml.cpp @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "GraphingNumPad.xaml.h" +#include "Views/NumberPad.xaml.h" +#include "Controls/CalculatorButton.h" +#include "CalcViewModel/Common/LocalizationSettings.h" +#include "Controls/MathRichEditBox.h" + +using namespace CalculatorApp; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Navigation; + +// Dictionary of the enum of the button clicked mapped to an object with the string to enter into the rich edit, and the start and end of the selection after text has been entered. +static const std::unordered_map> buttonOutput = { + { NumbersAndOperatorsEnum::Sin, { L"sin()", 4, 0 } }, + { NumbersAndOperatorsEnum::Cos, { L"cos()", 4, 0 } }, + { NumbersAndOperatorsEnum::Tan, { L"tan()", 4, 0 } }, + { NumbersAndOperatorsEnum::Sec, { L"sec()", 4, 0 } }, + { NumbersAndOperatorsEnum::Csc, { L"csc()", 4, 0 } }, + { NumbersAndOperatorsEnum::Cot, { L"cot()", 4, 0 } }, + { NumbersAndOperatorsEnum::InvSin, { L"arcsin()", 7, 0 } }, + { NumbersAndOperatorsEnum::InvCos, { L"arccos()", 7, 0 } }, + { NumbersAndOperatorsEnum::InvTan, { L"arctan()", 7, 0 } }, + { NumbersAndOperatorsEnum::InvSec, { L"arcsec()", 7, 0 } }, + { NumbersAndOperatorsEnum::InvCsc, { L"arccsc()", 7, 0 } }, + { NumbersAndOperatorsEnum::InvCot, { L"arccot()", 7, 0 } }, + { NumbersAndOperatorsEnum::Sinh, { L"sinh()", 5, 0 } }, + { NumbersAndOperatorsEnum::Cosh, { L"cosh()", 5, 0 } }, + { NumbersAndOperatorsEnum::Tanh, { L"tanh()", 5, 0 } }, + { NumbersAndOperatorsEnum::Sech, { L"sech()", 5, 0 } }, + { NumbersAndOperatorsEnum::Csch, { L"csch()", 5, 0 } }, + { NumbersAndOperatorsEnum::Coth, { L"coth()", 5, 0 } }, + { NumbersAndOperatorsEnum::InvSinh, { L"arcsinh()", 8, 0 } }, + { NumbersAndOperatorsEnum::InvCosh, { L"arccosh()", 8, 0 } }, + { NumbersAndOperatorsEnum::InvTanh, { L"arctanh()", 8, 0 } }, + { NumbersAndOperatorsEnum::InvSech, { L"arcsech()", 8, 0 } }, + { NumbersAndOperatorsEnum::InvCsch, { L"arccsch()", 8, 0 } }, + { NumbersAndOperatorsEnum::InvCoth, { L"arccoth()", 8, 0 } }, + { NumbersAndOperatorsEnum::Abs, { L"abs()", 4, 0 } }, + { NumbersAndOperatorsEnum::Floor, { L"floor()", 6, 0 } }, + { NumbersAndOperatorsEnum::Ceil, { L"ceiling()", 8, 0 } }, + { NumbersAndOperatorsEnum::Pi, { L"\u03C0", 1, 0 } }, + { NumbersAndOperatorsEnum::Euler, { L"e", 1, 0 } }, + { NumbersAndOperatorsEnum::XPower2, { L"^2", 2, 0 } }, + { NumbersAndOperatorsEnum::Cube, { L"^3", 2, 0 } }, + { NumbersAndOperatorsEnum::XPowerY, { L"^", 1, 0 } }, + { NumbersAndOperatorsEnum::TenPowerX, { L"10^", 3, 0 } }, + { NumbersAndOperatorsEnum::LogBase10, { L"log()", 4, 0 } }, + { NumbersAndOperatorsEnum::LogBaseE, { L"ln()", 3, 0 } }, + { NumbersAndOperatorsEnum::Sqrt, { L"sqrt()", 5, 0 } }, + { NumbersAndOperatorsEnum::CubeRoot, { L"cbrt()", 5, 0 } }, + { NumbersAndOperatorsEnum::YRootX, { L"root(x,n)", 7, 1 } }, + { NumbersAndOperatorsEnum::TwoPowerX, { L"2^", 2, 0 } }, + { NumbersAndOperatorsEnum::LogBaseX, { L"log(b, x)", 4, 1 } }, + { NumbersAndOperatorsEnum::EPowerX, { L"e^", 4, 0 } }, + { NumbersAndOperatorsEnum::Abs, { L"abs()", 4, 0 } }, + { NumbersAndOperatorsEnum::X, { L"x", 1, 0 } }, + { NumbersAndOperatorsEnum::Y, { L"y", 1, 0 } }, + { NumbersAndOperatorsEnum::OpenParenthesis, { L"(", 1, 0 } }, + { NumbersAndOperatorsEnum::CloseParenthesis, { L")", 1, 0 } }, + { NumbersAndOperatorsEnum::Equals, { L"=", 1, 0 } }, + { NumbersAndOperatorsEnum::Divide, { L"/", 1, 0 } }, + { NumbersAndOperatorsEnum::Multiply, { L"*", 1, 0 } }, + { NumbersAndOperatorsEnum::Subtract, { L"-", 1, 0 } }, + { NumbersAndOperatorsEnum::Add, { L"+", 1, 0 } }, + { NumbersAndOperatorsEnum::Invert, { L"1/", 2, 0 } }, + { NumbersAndOperatorsEnum::Negate, { L"-", 1, 0 } }, + { NumbersAndOperatorsEnum::GreaterThan, { L">", 1, 0 } }, + { NumbersAndOperatorsEnum::GreaterThanOrEqualTo, { L"\u2265", 1, 0 } }, + { NumbersAndOperatorsEnum::LessThan, { L"<", 1, 0 } }, + { NumbersAndOperatorsEnum::LessThanOrEqualTo, { L"\u2264", 1, 0 } }, + { NumbersAndOperatorsEnum::Zero, { L"0", 1, 0 } }, + { NumbersAndOperatorsEnum::One, { L"1", 1, 0 } }, + { NumbersAndOperatorsEnum::Two, { L"2", 1, 0 } }, + { NumbersAndOperatorsEnum::Three, { L"3", 1, 0 } }, + { NumbersAndOperatorsEnum::Four, { L"4", 1, 0 } }, + { NumbersAndOperatorsEnum::Five, { L"5", 1, 0 } }, + { NumbersAndOperatorsEnum::Six, { L"6", 1, 0 } }, + { NumbersAndOperatorsEnum::Seven, { L"7", 1, 0 } }, + { NumbersAndOperatorsEnum::Eight, { L"8", 1, 0 } }, + { NumbersAndOperatorsEnum::Nine, { L"9", 1, 0 } }, + { NumbersAndOperatorsEnum::Decimal, { L".", 1, 0 } }, +}; + +GraphingNumPad::GraphingNumPad() +{ + InitializeComponent(); + const auto& localizationSettings = CalculatorApp::Common::LocalizationSettings::GetInstance(); + DecimalSeparatorButton->Content = localizationSettings.GetDecimalSeparator(); + Num0Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('0'); + Num1Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('1'); + Num2Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('2'); + Num3Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('3'); + Num4Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('4'); + Num5Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('5'); + Num6Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('6'); + Num7Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('7'); + Num8Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('8'); + Num9Button->Content = localizationSettings.GetDigitSymbolFromEnUsDigit('9'); +} + +void GraphingNumPad::ShiftButton_Check(_In_ Platform::Object ^ /*sender*/, _In_ RoutedEventArgs ^ /*e*/) +{ + SetOperatorRowVisibility(); +} + +void GraphingNumPad::ShiftButton_Uncheck(_In_ Platform::Object ^ sender, _In_ RoutedEventArgs ^ /*e*/) +{ + ShiftButton->IsChecked = false; + SetOperatorRowVisibility(); + + GraphingNumPad::Button_Clicked(sender, nullptr); +} + +void GraphingNumPad::TrigFlyoutShift_Toggle(_In_ Platform::Object ^ /*sender*/, _In_ RoutedEventArgs ^ /*e*/) +{ + SetTrigRowVisibility(); +} + +void GraphingNumPad::TrigFlyoutHyp_Toggle(_In_ Platform::Object ^ /*sender*/, _In_ RoutedEventArgs ^ /*e*/) +{ + SetTrigRowVisibility(); +} + +void GraphingNumPad::FlyoutButton_Clicked(_In_ Platform::Object ^ sender, _In_ RoutedEventArgs ^ /*e*/) +{ + this->HypButton->IsChecked = false; + this->TrigShiftButton->IsChecked = false; + this->Trigflyout->Hide(); + this->FuncFlyout->Hide(); + this->InequalityFlyout->Hide(); + + GraphingNumPad::Button_Clicked(sender, nullptr); +} + +void GraphingNumPad::ShiftButton_IsEnabledChanged(_In_ Platform::Object ^ /*sender*/, _In_ DependencyPropertyChangedEventArgs ^ /*e*/) +{ + SetOperatorRowVisibility(); +} + +void GraphingNumPad::SetTrigRowVisibility() +{ + bool isShiftChecked = TrigShiftButton->IsChecked->Value; + bool isHypeChecked = HypButton->IsChecked->Value; + + InverseHyperbolicTrigFunctions->Visibility = ::Visibility::Collapsed; + InverseTrigFunctions->Visibility = ::Visibility::Collapsed; + HyperbolicTrigFunctions->Visibility = ::Visibility::Collapsed; + TrigFunctions->Visibility = ::Visibility::Collapsed; + + if (isShiftChecked && isHypeChecked) + { + InverseHyperbolicTrigFunctions->Visibility = ::Visibility::Visible; + } + else if (isShiftChecked && !isHypeChecked) + { + InverseTrigFunctions->Visibility = ::Visibility::Visible; + } + else if (!isShiftChecked && isHypeChecked) + { + HyperbolicTrigFunctions->Visibility = ::Visibility::Visible; + } + else + { + TrigFunctions->Visibility = ::Visibility::Visible; + } +} + +void GraphingNumPad::SetOperatorRowVisibility() +{ + ::Visibility rowVis, invRowVis; + if (ShiftButton->IsChecked->Value) + { + rowVis = ::Visibility::Collapsed; + invRowVis = ::Visibility::Visible; + } + else + { + rowVis = ::Visibility::Visible; + invRowVis = ::Visibility::Collapsed; + } + + Row1->Visibility = rowVis; + InvRow1->Visibility = invRowVis; +} + +void GraphingNumPad::Button_Clicked(Platform::Object ^ sender, DependencyPropertyChangedEventArgs ^ /*e*/) +{ + auto mathRichEdit = GetActiveRichEdit(); + auto button = dynamic_cast(sender); + if (mathRichEdit != nullptr && sender != nullptr) + { + auto id = button->ButtonId; + auto output = buttonOutput.find(id); + mathRichEdit->InsertText(std::get<0>(output->second), std::get<1>(output->second), std::get<2>(output->second)); + } +} + +void GraphingNumPad::SubmitButton_Clicked(Platform::Object ^ /*sender*/, RoutedEventArgs ^ /*e*/) +{ + auto mathRichEdit = GetActiveRichEdit(); + if (mathRichEdit != nullptr) + { + mathRichEdit->SubmitEquation(CalculatorApp::Controls::EquationSubmissionSource::ENTER_KEY); + } +} + +void GraphingNumPad::ClearButton_Clicked(Platform::Object ^ /*sender*/, RoutedEventArgs ^ /*e*/) +{ + auto mathRichEdit = GetActiveRichEdit(); + if (mathRichEdit != nullptr) + { + mathRichEdit->MathText = L""; + mathRichEdit->SubmitEquation(CalculatorApp::Controls::EquationSubmissionSource::PROGRAMMATIC); + } +} + +void GraphingNumPad::BackSpaceButton_Clicked(Platform::Object ^ /*sender*/, RoutedEventArgs ^ /*e*/) +{ + auto mathRichEdit = GetActiveRichEdit(); + if (mathRichEdit != nullptr) + { + mathRichEdit->BackSpace(); + } +} + +// To avoid focus moving when the space between buttons is clicked, handle click events that make it through the keypad. +void GraphingNumPad::GraphingNumPad_PointerPressed(Platform::Object ^ /*sender*/, PointerRoutedEventArgs ^ e) +{ + e->Handled = true; +} + +CalculatorApp::Controls::MathRichEditBox ^ GraphingNumPad::GetActiveRichEdit() +{ + return dynamic_cast(FocusManager::GetFocusedElement()); +} + +// Adding event because the ShowMode property is ignored in xaml. +void GraphingNumPad::Flyout_Opening(Platform::Object ^ sender, Platform::Object ^ /*e*/) +{ + auto flyout = dynamic_cast(sender); + if (flyout) + { + flyout->ShowMode = FlyoutShowMode::Transient; + } +} diff --git a/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml.h b/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml.h new file mode 100644 index 000000000..37ec8a31c --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingNumPad.xaml.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Views\GraphingCalculator\GraphingNumPad.g.h" +#include "CalcViewModel/GraphingCalculator/GraphingCalculatorViewModel.h" +#include "Views/GraphingCalculator/EquationInputArea.xaml.h" +#include "CalcViewModel/Common/CalculatorButtonUser.h" + +namespace CalculatorApp +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class GraphingNumPad sealed + { + public: + GraphingNumPad(); + + private: + void ShiftButton_Check(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e); + void ShiftButton_Uncheck(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e); + void TrigFlyoutShift_Toggle(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e); + void TrigFlyoutHyp_Toggle(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e); + void FlyoutButton_Clicked(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e); + void ShiftButton_IsEnabledChanged(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::DependencyPropertyChangedEventArgs ^ e); + void SetOperatorRowVisibility(); + void SetTrigRowVisibility(); + void Button_Clicked(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::DependencyPropertyChangedEventArgs ^ e); + void SubmitButton_Clicked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void ClearButton_Clicked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void BackSpaceButton_Clicked(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + void GraphingNumPad_PointerPressed(Platform::Object ^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e); + Controls::MathRichEditBox^ GetActiveRichEdit(); + void Flyout_Opening(Platform::Object ^ sender, Platform::Object ^ e); + }; +} diff --git a/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml b/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml new file mode 100644 index 000000000..58451b590 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml.cpp b/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml.cpp new file mode 100644 index 000000000..c1ee621e4 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml.cpp @@ -0,0 +1,94 @@ +#include "pch.h" + +#include "GraphingSettings.xaml.h" +#include "CalcViewModel\Common\AppResourceProvider.cpp" + +using namespace std; +using namespace Graphing; +using namespace GraphControl; + +using namespace CalculatorApp; +using namespace CalculatorApp::ViewModel; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::System; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Navigation; + +GraphingSettings::GraphingSettings() + : m_ViewModel(ref new GraphingSettingsViewModel()) +{ + InitializeComponent(); +} + +void GraphingSettings::SetGrapher(Grapher ^ grapher) +{ + m_ViewModel->SetGrapher(grapher); +} + +Style ^ GraphingSettings::SelectTextBoxStyle(bool incorrectRange, bool error) +{ + if (incorrectRange || error) + { + return static_cast<::Style ^>(this->Resources->Lookup(L"ErrorTextBoxStyle")); + } + else + { + return nullptr; + } +} + +void GraphingSettings::GridSettingsTextBox_PreviewKeyDown(Platform::Object ^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs ^ e) +{ + if (e->Key == VirtualKey::Enter) + { + if (!FocusManager::TryMoveFocusAsync(FocusNavigationDirection::Next)) + { + FocusManager::TryMoveFocusAsync(FocusNavigationDirection::Previous); + } + e->Handled = true; + } +} + +bool GraphingSettings::CanBeClose() +{ + auto focusedElement = FocusManager::GetFocusedElement(); + + // Move focus so we are sure all values are in sync with the VM + if (focusedElement != nullptr) + { + if (focusedElement->Equals(SettingsXMin)) + { + auto textbox = static_cast(focusedElement); + ViewModel->XMin = textbox->Text; + } + else if (focusedElement->Equals(SettingsXMax)) + { + auto textbox = static_cast(focusedElement); + ViewModel->XMax = textbox->Text; + } + else if (focusedElement->Equals(SettingsYMin)) + { + auto textbox = static_cast(focusedElement); + ViewModel->YMin = textbox->Text; + } + else if (focusedElement->Equals(SettingsYMax)) + { + auto textbox = static_cast(focusedElement); + ViewModel->YMax = textbox->Text; + } + } + return ViewModel != nullptr && ViewModel->HasError(); +} + +void GraphingSettings::RefreshRanges() +{ + ViewModel->InitRanges(); +} diff --git a/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml.h b/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml.h new file mode 100644 index 000000000..e726b4fa9 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/GraphingSettings.xaml.h @@ -0,0 +1,28 @@ +// +// MyUserControl.xaml.h +// Declaration of the MyUserControl class +// + +#pragma once + +#include "CalcViewModel/Common/Utils.h" +#include "CalcViewModel/GraphingCalculator/GraphingSettingsViewModel.h" +#include "Views\GraphingCalculator\GraphingSettings.g.h" +#include + +namespace CalculatorApp +{ + [Windows::Foundation::Metadata::WebHostHidden] public ref class GraphingSettings sealed + { + public: + GraphingSettings(); + + PROPERTY_R(CalculatorApp::ViewModel::GraphingSettingsViewModel ^, ViewModel); + Windows::UI::Xaml::Style ^ SelectTextBoxStyle(bool incorrectRange, bool error); + void SetGrapher(GraphControl::Grapher ^ grapher); + bool CanBeClose(); + void RefreshRanges(); + private: + void GridSettingsTextBox_PreviewKeyDown(Platform::Object ^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs ^ e); + }; +} diff --git a/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml b/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml new file mode 100644 index 000000000..c7b43c328 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml.cpp b/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml.cpp new file mode 100644 index 000000000..45e8959a6 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml.cpp @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "KeyGraphFeaturesPanel.xaml.h" +#include "Controls/MathRichEditBox.h" +#include "CalcViewModel/GraphingCalculatorEnums.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +KeyGraphFeaturesPanel::KeyGraphFeaturesPanel() +{ + InitializeComponent(); + this->Loaded += ref new RoutedEventHandler(this, &KeyGraphFeaturesPanel::KeyGraphFeaturesPanel_Loaded); +} + +void KeyGraphFeaturesPanel::KeyGraphFeaturesPanel_Loaded(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e) +{ + BackButton->Focus(::FocusState::Programmatic); +} + +void KeyGraphFeaturesPanel::BackButton_Click(Object ^ sender, RoutedEventArgs ^ e) +{ + KeyGraphFeaturesClosed(this, ref new RoutedEventArgs()); +} diff --git a/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml.h b/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml.h new file mode 100644 index 000000000..61fe31713 --- /dev/null +++ b/src/Calculator/Views/GraphingCalculator/KeyGraphFeaturesPanel.xaml.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Views\GraphingCalculator\KeyGraphFeaturesPanel.g.h" +#include "CalcViewModel\GraphingCalculator\EquationViewModel.h" +#include "Controls/MathRichEditBox.h" +#include "Controls/EquationTextBox.h" +#include "TemplateSelectors/KeyGraphFeaturesTemplateSelector.h" + +namespace CalculatorApp +{ +public + ref class KeyGraphFeaturesPanel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + KeyGraphFeaturesPanel(); + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(CalculatorApp::ViewModel::EquationViewModel ^, ViewModel); + + public: + event Windows::UI::Xaml::RoutedEventHandler ^ KeyGraphFeaturesClosed; + + static Windows::UI::Xaml::Media::SolidColorBrush + ^ ToSolidColorBrush(Windows::UI::Color color) { return ref new Windows::UI::Xaml::Media::SolidColorBrush(color); } + private: + void KeyGraphFeaturesPanel_Loaded(_In_ Platform::Object ^ sender, _In_ Windows::UI::Xaml::RoutedEventArgs ^ e); + void BackButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + + private: + CalculatorApp::ViewModel::EquationViewModel ^ m_viewModel; + }; +} diff --git a/src/Calculator/Views/MainPage.xaml b/src/Calculator/Views/MainPage.xaml index ac76ac764..1d017727f 100644 --- a/src/Calculator/Views/MainPage.xaml +++ b/src/Calculator/Views/MainPage.xaml @@ -86,8 +86,9 @@ Command="{x:Bind Model.PasteCommand}"/> - + @@ -96,6 +97,9 @@ + + + @@ -108,7 +112,6 @@ DataContext="{x:Bind Model}" ExpandedModeThresholdWidth="Infinity" IsBackButtonVisible="Collapsed" - IsEnabled="{x:Bind Model.IsAlwaysOnTop, Converter={StaticResource BooleanNegationConverter}, Mode=OneWay}" IsPaneToggleButtonVisible="{x:Bind Model.IsAlwaysOnTop, Converter={StaticResource BooleanNegationConverter}, Mode=OneWay}" IsSettingsVisible="False" ItemInvoked="OnNavItemInvoked" @@ -126,9 +129,9 @@ + ItemClick="OnAboutButtonClick" + x:Load="False"> @@ -180,7 +183,7 @@ Content="" Visibility="{x:Bind Model.DisplayNormalAlwaysOnTopOption, Mode=OneWay}"> - + diff --git a/src/Calculator/Views/MainPage.xaml.cpp b/src/Calculator/Views/MainPage.xaml.cpp index c7ba04ee5..64a9ed548 100644 --- a/src/Calculator/Views/MainPage.xaml.cpp +++ b/src/Calculator/Views/MainPage.xaml.cpp @@ -168,6 +168,10 @@ void MainPage::OnAppPropertyChanged(_In_ Platform::Object ^ sender, _In_ Windows } EnsureDateCalculator(); } + else if (newValue == ViewMode::Graphing) + { + EnsureGraphingCalculator(); + } else if (NavCategory::IsConverterViewMode(newValue)) { if (m_model->CalculatorViewModel) @@ -198,6 +202,7 @@ void MainPage::ShowHideControls(ViewMode mode) { auto isCalcViewMode = NavCategory::IsCalculatorViewMode(mode); auto isDateCalcViewMode = NavCategory::IsDateCalculatorViewMode(mode); + auto isGraphingCalcViewMode = NavCategory::IsGraphingCalculatorViewMode(mode); auto isConverterViewMode = NavCategory::IsConverterViewMode(mode); if (m_calculator) @@ -212,6 +217,12 @@ void MainPage::ShowHideControls(ViewMode mode) m_dateCalculator->IsEnabled = isDateCalcViewMode; } + if (m_graphingCalculator) + { + m_graphingCalculator->Visibility = BooleanToVisibilityConverter::Convert(isGraphingCalcViewMode); + m_graphingCalculator->IsEnabled = isGraphingCalcViewMode; + } + if (m_converter) { m_converter->Visibility = BooleanToVisibilityConverter::Convert(isConverterViewMode); @@ -239,7 +250,7 @@ void MainPage::UpdatePanelViewState() void MainPage::OnPageLoaded(_In_ Object ^, _In_ RoutedEventArgs ^ args) { - if (!m_converter && !m_calculator && !m_dateCalculator) + if (!m_converter && !m_calculator && !m_dateCalculator && !m_graphingCalculator) { // We have just launched into our default mode (standard calc) so ensure calc is loaded EnsureCalculator(); @@ -284,6 +295,10 @@ void MainPage::SetDefaultFocus() { m_dateCalculator->SetDefaultFocus(); } + if (m_graphingCalculator != nullptr && m_graphingCalculator->Visibility == ::Visibility::Visible) + { + FocusManager::TryFocusAsync(m_graphingCalculator, ::FocusState::Programmatic); + } if (m_converter != nullptr && m_converter->Visibility == ::Visibility::Visible) { m_converter->SetDefaultFocus(); @@ -345,6 +360,18 @@ void MainPage::EnsureDateCalculator() } } +void MainPage::EnsureGraphingCalculator() +{ + if (!m_graphingCalculator) + { + m_graphingCalculator = ref new GraphingCalculator(); + m_graphingCalculator->Name = L"GraphingCalculator"; + m_graphingCalculator->DataContext = m_model->GraphingCalcViewModel; + + GraphingCalcHolder->Child = m_graphingCalculator; + } +} + void MainPage::EnsureConverter() { if (!m_converter) @@ -473,6 +500,7 @@ MUXC::NavigationViewItem ^ MainPage::CreateNavViewItemFromCategory(NavCategory ^ item->Content = category->Name; item->AccessKey = category->AccessKey; + item->IsEnabled = category->IsEnabled; item->Style = static_cast(Resources->Lookup(L"NavViewItemStyle")); AutomationProperties::SetName(item, category->AutomationName); @@ -517,7 +545,7 @@ void MainPage::SetHeaderAutomationName() else { String ^ full; - if (NavCategory::IsCalculatorViewMode(mode)) + if (NavCategory::IsCalculatorViewMode(mode) || NavCategory::IsGraphingCalculatorViewMode(mode)) { full = resProvider->GetResourceString(L"HeaderAutomationName_Calculator"); } diff --git a/src/Calculator/Views/MainPage.xaml.h b/src/Calculator/Views/MainPage.xaml.h index c7a2e98f8..f2c75d6a0 100644 --- a/src/Calculator/Views/MainPage.xaml.h +++ b/src/Calculator/Views/MainPage.xaml.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 @@ -6,6 +6,7 @@ #include "Views/Calculator.xaml.h" #include "Views/MainPage.g.h" #include "Views/DateCalculator.xaml.h" +#include "Views/GraphingCalculator/GraphingCalculator.xaml.h" #include "Views/UnitConverter.xaml.h" #include "CalcViewModel/ApplicationViewModel.h" @@ -71,13 +72,15 @@ public void App_Suspending(Object ^ sender, Windows::ApplicationModel::SuspendingEventArgs ^ e); void EnsureCalculator(); - void EnsureConverter(); void EnsureDateCalculator(); + void EnsureGraphingCalculator(); + void EnsureConverter(); void ShowAboutPage(); void AnnounceCategoryName(); CalculatorApp::Calculator ^ m_calculator; + GraphingCalculator^ m_graphingCalculator; CalculatorApp::UnitConverter ^ m_converter; CalculatorApp::DateCalculator ^ m_dateCalculator; Windows::Foundation::EventRegistrationToken m_windowSizeEventToken; diff --git a/src/Calculator/pch.h b/src/Calculator/pch.h index add4efde6..d53bce713 100644 --- a/src/Calculator/pch.h +++ b/src/Calculator/pch.h @@ -27,6 +27,8 @@ #include #include #include +#include +#include // C++\WinRT Headers #include "winrt/base.h" diff --git a/src/CalculatorUnitTests/AsyncHelper.cpp b/src/CalculatorUnitTests/AsyncHelper.cpp new file mode 100644 index 000000000..73937a55c --- /dev/null +++ b/src/CalculatorUnitTests/AsyncHelper.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "AsyncHelper.h" +#include +#include + +using namespace std; +using namespace concurrency; +using namespace Platform; +using namespace CalculatorApp; +using namespace Windows::UI::Core; +using namespace Windows::ApplicationModel::Core; + +task AsyncHelper::RunOnUIThreadAsync(function&& action) +{ + auto callback = ref new DispatchedHandler([action]() { action(); }); + + return create_task(CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, callback)); +} + +void AsyncHelper::RunOnUIThread(function&& action, DWORD timeout) +{ + task waitTask = RunOnUIThreadAsync([action]() { action(); }); + + WaitForTask(waitTask, timeout); +} + +void AsyncHelper::Delay(DWORD milliseconds) +{ + thread timer(bind(CalculatorApp::AsyncHelper::Sleep, milliseconds)); + timer.join(); +} + +void AsyncHelper::Sleep(DWORD milliseconds) +{ + this_thread::sleep_for(chrono::milliseconds(milliseconds)); +} diff --git a/src/CalculatorUnitTests/AsyncHelper.h b/src/CalculatorUnitTests/AsyncHelper.h new file mode 100644 index 000000000..db52809a3 --- /dev/null +++ b/src/CalculatorUnitTests/AsyncHelper.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include + +namespace CalculatorApp +{ + class AsyncHelper + { + public: + static concurrency::task RunOnUIThreadAsync(std::function&& action); + static void RunOnUIThread(std::function&& action, DWORD timeout = INFINITE); + static void Delay(DWORD milliseconds); + + template + static void RunOnUIThread(std::function()>&& action, DWORD timeout = INFINITE) + { + concurrency::task t; + concurrency::task uiTask = + RunOnUIThreadAsync([&t, action]() { t = action(); }).then([&t]() { t.wait(); }, concurrency::task_continuation_context::use_arbitrary()); + + WaitForTask(uiTask, timeout); + } + + template + static bool WaitForTask(concurrency::task& t, DWORD timeout = INFINITE) + { + Microsoft::WRL::Wrappers::Event event(CreateEventEx(nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS)); + if (!event.IsValid()) + { + throw std::bad_alloc(); + } + + Platform::Exception ^ ex; + t.then( + [&event, &ex](concurrency::task prevTask) { + try + { + prevTask.get(); + } + catch (Platform::Exception ^ e) + { + ex = e; + } + + if (event.IsValid()) + { + SetEvent(event.Get()); + } + }, + concurrency::task_continuation_context::use_arbitrary()); + + DWORD waitResult; // = STATUS_PENDING; + waitResult = WaitForSingleObjectEx(event.Get(), timeout, true); + event.Close(); + + if (ex != nullptr) + { + throw ex; + } + + if (waitResult == WAIT_FAILED) + { + throw ref new Platform::Exception(-1, L"Error in waiting for task completion: " + waitResult.ToString()); + } + + return waitResult == WAIT_OBJECT_0; + } + + private: + static void Sleep(DWORD milliseconds); + }; +} diff --git a/src/CalculatorUnitTests/CalculatorUnitTests.rc b/src/CalculatorUnitTests/CalculatorUnitTests.rc new file mode 100644 index 000000000..6d2853291 Binary files /dev/null and b/src/CalculatorUnitTests/CalculatorUnitTests.rc differ diff --git a/src/CalculatorUnitTests/CurrencyConverterUnitTests.cpp b/src/CalculatorUnitTests/CurrencyConverterUnitTests.cpp index 8c6f5e2ff..a5347227d 100644 --- a/src/CalculatorUnitTests/CurrencyConverterUnitTests.cpp +++ b/src/CalculatorUnitTests/CurrencyConverterUnitTests.cpp @@ -103,8 +103,6 @@ namespace CalculatorUnitTests { constexpr auto sc_Language_EN = L"en-US"; - const UCM::Category CURRENCY_CATEGORY = { NavCategory::Serialize(ViewMode::Currency), L"Currency", false /*supportsNegative*/ }; - unique_ptr MakeLoaderWithResults(String ^ staticResponse, String ^ allRatiosResponse) { auto client = make_unique(staticResponse, allRatiosResponse); @@ -382,8 +380,12 @@ TEST_METHOD(Load_Success_LoadedFromWeb) } ; -TEST_CLASS(CurrencyConverterUnitTests){ const UCM::Unit GetUnit(const vector& unitList, const wstring& target){ - return *find_if(begin(unitList), end(unitList), [&target](const UCM::Unit& u) { return u.abbreviation == target; }); +TEST_CLASS(CurrencyConverterUnitTests){ + + const UCM::Category CURRENCY_CATEGORY = { NavCategory::Serialize(ViewMode::Currency), L"Currency", false /*supportsNegative*/ }; + + const UCM::Unit GetUnit(const vector& unitList, const wstring& target){ + return *find_if(begin(unitList), end(unitList), [&target](const UCM::Unit& u) { return u.abbreviation == target; }); } TEST_METHOD(Loaded_LoadOrderedUnits) diff --git a/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp b/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp index a6a7ea041..3d51ef816 100644 --- a/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp +++ b/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp @@ -48,15 +48,377 @@ namespace DateCalculationUnitTests static DateCalculationEngine ^ m_DateCalcEngine; public: - TEST_CLASS_INITIALIZE(TestClassSetup) - { - m_DateCalcEngine = ref new DateCalculationEngine(CalendarIdentifiers::Gregorian); - - /* Test Case Data */ - - // Dates - DD.MM.YYYY - /*31.12.9999*/ - date[0].wYear = 9999; + TEST_CLASS_INITIALIZE(TestClassSetup) + { + m_DateCalcEngine = ref new DateCalculationEngine(CalendarIdentifiers::Gregorian); + + /* Test Case Data */ + + // Dates - DD.MM.YYYY + /*31.12.9999*/ + date[0].wYear = 9999; + date[0].wMonth = 12; + date[0].wDayOfWeek = 5; + date[0].wDay = 31; + date[0].wHour = 0; + date[0].wMinute = 0; + date[0].wSecond = 0; + date[0].wMilliseconds = 0; + /*30.12.9999*/ date[1].wYear = 9999; + date[1].wMonth = 12; + date[1].wDayOfWeek = 4; + date[1].wDay = 30; + date[1].wHour = 0; + date[1].wMinute = 0; + date[1].wSecond = 0; + date[1].wMilliseconds = 0; + /*31.12.9998*/ date[2].wYear = 9998; + date[2].wMonth = 12; + date[2].wDayOfWeek = 4; + date[2].wDay = 31; + date[2].wHour = 0; + date[2].wMinute = 0; + date[2].wSecond = 0; + date[2].wMilliseconds = 0; + /*01.01.1601*/ date[3].wYear = 1601; + date[3].wMonth = 1; + date[3].wDayOfWeek = 1; + date[3].wDay = 1; + date[3].wHour = 0; + date[3].wMinute = 0; + date[3].wSecond = 0; + date[3].wMilliseconds = 0; + /*02.01.1601*/ date[4].wYear = 1601; + date[4].wMonth = 1; + date[4].wDayOfWeek = 2; + date[4].wDay = 2; + date[4].wHour = 0; + date[4].wMinute = 0; + date[4].wSecond = 0; + date[4].wMilliseconds = 0; + /*10.05.2008*/ date[5].wYear = 2008; + date[5].wMonth = 5; + date[5].wDayOfWeek = 6; + date[5].wDay = 10; + date[5].wHour = 0; + date[5].wMinute = 0; + date[5].wSecond = 0; + date[5].wMilliseconds = 0; + /*10.03.2008*/ date[6].wYear = 2008; + date[6].wMonth = 3; + date[6].wDayOfWeek = 1; + date[6].wDay = 10; + date[6].wHour = 0; + date[6].wMinute = 0; + date[6].wSecond = 0; + date[6].wMilliseconds = 0; + /*29.02.2008*/ date[7].wYear = 2008; + date[7].wMonth = 2; + date[7].wDayOfWeek = 5; + date[7].wDay = 29; + date[7].wHour = 0; + date[7].wMinute = 0; + date[7].wSecond = 0; + date[7].wMilliseconds = 0; + /*28.02.2007*/ date[8].wYear = 2007; + date[8].wMonth = 2; + date[8].wDayOfWeek = 3; + date[8].wDay = 28; + date[8].wHour = 0; + date[8].wMinute = 0; + date[8].wSecond = 0; + date[8].wMilliseconds = 0; + /*10.03.2007*/ date[9].wYear = 2007; + date[9].wMonth = 3; + date[9].wDayOfWeek = 6; + date[9].wDay = 10; + date[9].wHour = 0; + date[9].wMinute = 0; + date[9].wSecond = 0; + date[9].wMilliseconds = 0; + /*10.05.2007*/ date[10].wYear = 2007; + date[10].wMonth = 5; + date[10].wDayOfWeek = 4; + date[10].wDay = 10; + date[10].wHour = 0; + date[10].wMinute = 0; + date[10].wSecond = 0; + date[10].wMilliseconds = 0; + /*29.01.2008*/ date[11].wYear = 2008; + date[11].wMonth = 1; + date[11].wDayOfWeek = 2; + date[11].wDay = 29; + date[11].wHour = 0; + date[11].wMinute = 0; + date[11].wSecond = 0; + date[11].wMilliseconds = 0; + /*28.01.2007*/ date[12].wYear = 2007; + date[12].wMonth = 1; + date[12].wDayOfWeek = 0; + date[12].wDay = 28; + date[12].wHour = 0; + date[12].wMinute = 0; + date[12].wSecond = 0; + date[12].wMilliseconds = 0; + /*31.01.2008*/ date[13].wYear = 2008; + date[13].wMonth = 1; + date[13].wDayOfWeek = 4; + date[13].wDay = 31; + date[13].wHour = 0; + date[13].wMinute = 0; + date[13].wSecond = 0; + date[13].wMilliseconds = 0; + /*31.03.2008*/ date[14].wYear = 2008; + date[14].wMonth = 3; + date[14].wDayOfWeek = 1; + date[14].wDay = 31; + date[14].wHour = 0; + date[14].wMinute = 0; + date[14].wSecond = 0; + date[14].wMilliseconds = 0; + + // Date Differences + dateDifference[0].year = 1; + dateDifference[0].month = 1; + dateDifference[1].month = 1; + dateDifference[1].day = 10; + dateDifference[2].day = 2; + /*date[2]-[0]*/ dateDifference[3].week = 52; + dateDifference[3].day = 1; + /*date[2]-[0]*/ dateDifference[4].year = 1; + dateDifference[5].day = 365; + dateDifference[6].month = 1; + dateDifference[7].month = 1; + dateDifference[7].day = 2; + dateDifference[8].day = 31; + dateDifference[9].month = 11; + dateDifference[9].day = 1; + dateDifference[10].year = 8398; + dateDifference[10].month = 11; + dateDifference[10].day = 30; + dateDifference[11].year = 2008; + dateDifference[12].year = 7991; + dateDifference[12].month = 11; + dateDifference[13].week = 416998; + dateDifference[13].day = 1; + + /* Test Cases */ + + // Date Difference test cases + datetimeDifftest[0].startDate = date[0]; + datetimeDifftest[0].endDate = date[3]; + datetimeDifftest[0].dateDiff = dateDifference[10]; + datetimeDifftest[1].startDate = date[0]; + datetimeDifftest[1].endDate = date[2]; + datetimeDifftest[1].dateDiff = dateDifference[5]; + datetimeDifftest[2].startDate = date[0]; + datetimeDifftest[2].endDate = date[2]; + datetimeDifftest[2].dateDiff = dateDifference[4]; + datetimeDifftest[3].startDate = date[0]; + datetimeDifftest[3].endDate = date[2]; + datetimeDifftest[3].dateDiff = dateDifference[3]; + datetimeDifftest[4].startDate = date[14]; + datetimeDifftest[4].endDate = date[7]; + datetimeDifftest[4].dateDiff = dateDifference[7]; + datetimeDifftest[5].startDate = date[14]; + datetimeDifftest[5].endDate = date[7]; + datetimeDifftest[5].dateDiff = dateDifference[8]; + datetimeDifftest[6].startDate = date[11]; + datetimeDifftest[6].endDate = date[8]; + datetimeDifftest[6].dateDiff = dateDifference[9]; + datetimeDifftest[7].startDate = date[13]; + datetimeDifftest[7].endDate = date[0]; + datetimeDifftest[7].dateDiff = dateDifference[12]; + datetimeDifftest[8].startDate = date[13]; + datetimeDifftest[8].endDate = date[0]; + datetimeDifftest[8].dateDiff = dateDifference[13]; + + // Date Add Out of Bound test cases (Negative tests) + /*OutofBound*/ datetimeBoundAdd[0].startDate = date[1]; + datetimeBoundAdd[0].endDate = date[0]; + datetimeBoundAdd[0].dateDiff = dateDifference[2]; // on Add date[0] not used + /*OutofBound*/ datetimeBoundAdd[1].startDate = date[2]; + datetimeBoundAdd[1].endDate = date[0]; + datetimeBoundAdd[1].dateDiff = dateDifference[11]; // on Add date[0] not used + + // Date Subtract Out of Bound test cases (Negative tests) + /*OutofBound*/ datetimeBoundSubtract[0].startDate = date[3]; + datetimeBoundSubtract[0].endDate = date[0]; + datetimeBoundSubtract[0].dateDiff = dateDifference[2]; // on subtract date[0] not used + /*OutofBound*/ datetimeBoundSubtract[1].startDate = date[14]; + datetimeBoundSubtract[1].endDate = date[0]; + datetimeBoundSubtract[1].dateDiff = dateDifference[11]; // on subtract date[0] not used + + // Date Add test cases (Positive tests) + datetimeAddCase[0].startDate = date[13]; + datetimeAddCase[0].endDate = date[7]; + datetimeAddCase[0].dateDiff = dateDifference[6]; // add + datetimeAddCase[1].startDate = date[14]; + datetimeAddCase[1].endDate = date[5]; + datetimeAddCase[1].dateDiff = dateDifference[1]; // add + datetimeAddCase[2].startDate = date[13]; + datetimeAddCase[2].endDate = date[6]; + datetimeAddCase[2].dateDiff = dateDifference[1]; // add + + // Date Subtract test cases (Positive tests) + datetimeSubtractCase[0].startDate = date[14]; + datetimeSubtractCase[0].endDate = date[7]; + datetimeSubtractCase[0].dateDiff = dateDifference[6]; // subtract + datetimeSubtractCase[1].startDate = date[6]; + datetimeSubtractCase[1].endDate = date[11]; + datetimeSubtractCase[1].dateDiff = dateDifference[1]; // subtract + datetimeSubtractCase[2].startDate = date[9]; + datetimeSubtractCase[2].endDate = date[12]; + datetimeSubtractCase[2].dateDiff = dateDifference[1]; // subtract + } + + /* Duration Between Two Date Tests -- Timediff obtained after calculation should be checked to be identical */ + TEST_METHOD(TestDateDiff) + { + // TODO - MSFT 10331900, fix this test + + // for (int testIndex = 0; testIndex < c_diffTestCase; testIndex++) + //{ + // DateDifference diff; + // DateUnit dateOutputFormat; + + // switch (testIndex) + // { + // case 0: + // case 2: + // dateOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Day; + // break; + // case 1: + // dateOutputFormat = DateUnit::Day; + // break; + // case 3: + // case 8: + // dateOutputFormat = DateUnit::Week | DateUnit::Day; + // break; + // case 7: + // dateOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Day; + // break; + // case 4: + // case 6: + // dateOutputFormat = DateUnit::Month | DateUnit::Day; + // break; + // case 5: + // dateOutputFormat = DateUnit::Day; + // break; + // } + + // // Calculate the difference + // m_DateCalcEngine->TryGetDateDifference(DateUtils::SystemTimeToDateTime(datetimeDifftest[testIndex].startDate), + // DateUtils::SystemTimeToDateTime(datetimeDifftest[testIndex].endDate), dateOutputFormat, &diff); + + // // Assert for the result + // bool areIdentical = true; + // if (diff.year != datetimeDifftest[testIndex].dateDiff.year || + // diff.month != datetimeDifftest[testIndex].dateDiff.month || + // diff.week != datetimeDifftest[testIndex].dateDiff.week || + // diff.day != datetimeDifftest[testIndex].dateDiff.day) + // { + // areIdentical = false; + // } + + // VERIFY_IS_TRUE(areIdentical); + //} + } + + /*Add Out of bound Tests*/ + TEST_METHOD(TestAddOob) + { + // TODO - MSFT 10331900, fix this test + + // for (int testIndex = 0; testIndex< c_numAddOobDate; testIndex++) + //{ + // DateTime endDate; + + // // Add Duration + // bool isValid = m_DateCalcEngine->AddDuration(DateUtils::SystemTimeToDateTime(datetimeBoundAdd[testIndex].startDate), + // datetimeBoundAdd[testIndex].dateDiff, &endDate); + + // // Assert for the result + // VERIFY_IS_FALSE(isValid); + //} + } + + /*Subtract Out of bound Tests*/ + TEST_METHOD(TestSubtractOob) + { + for (int testIndex = 0; testIndex < c_numSubtractOobDate; testIndex++) + { + // Subtract Duration + auto endDate = m_DateCalcEngine->SubtractDuration( + DateUtils::SystemTimeToDateTime(datetimeBoundSubtract[testIndex].startDate), datetimeBoundSubtract[testIndex].dateDiff); + + // Assert for the result + VERIFY_IS_NULL(endDate); + } + } + + // Add Tests + TEST_METHOD(TestAddition) + { + // TODO - MSFT 10331900, fix this test + + // for (int testIndex = 0; testIndex < c_addCases; testIndex++) + //{ + // DateTime endDate; + + // // Add Duration + // bool isValid = m_DateCalcEngine->AddDuration(DateUtils::SystemTimeToDateTime(datetimeAddCase[testIndex].startDate), + // datetimeAddCase[testIndex].dateDiff, &endDate); + + // // Assert for the result + // VERIFY_IS_TRUE(isValid); + + // SYSTEMTIME systemTime = DateUtils::DateTimeToSystemTime(endDate); + // if (systemTime.wYear != datetimeAddCase[testIndex].endDate.wYear || + // systemTime.wMonth != datetimeAddCase[testIndex].endDate.wMonth || + // systemTime.wDay != datetimeAddCase[testIndex].endDate.wDay || + // systemTime.wDayOfWeek != datetimeAddCase[testIndex].endDate.wDayOfWeek) + // { + // isValid = false; + // } + + // VERIFY_IS_TRUE(isValid); + //} + } + + // Subtract Tests + TEST_METHOD(TestSubtraction) + { + // TODO - MSFT 10331900, fix this test + + // for (int testIndex = 0; testIndex < c_subtractCases; testIndex++) + //{ + // DateTime endDate; + + // // Subtract Duration + // bool isValid = m_DateCalcEngine->SubtractDuration(DateUtils::SystemTimeToDateTime(datetimeSubtractCase[testIndex].startDate), + // datetimeSubtractCase[testIndex].dateDiff, &endDate); + + // // assert for the result + // VERIFY_IS_TRUE(isValid); + + // SYSTEMTIME systemTime = DateUtils::DateTimeToSystemTime(endDate); + // if (systemTime.wYear != datetimeSubtractCase[testIndex].endDate.wYear || + // systemTime.wMonth != datetimeSubtractCase[testIndex].endDate.wMonth || + // systemTime.wDay != datetimeSubtractCase[testIndex].endDate.wDay || + // systemTime.wDayOfWeek != datetimeSubtractCase[testIndex].endDate.wDayOfWeek) + // { + // isValid = false; + // } + + // VERIFY_IS_TRUE(isValid); + //} + } + }; + + TEST_CLASS(DateCalculatorViewModelTests){ public: TEST_CLASS_INITIALIZE(TestClassSetup){ /* Test Case Data */ + // Dates - DD.MM.YYYY + /*31.12.9999*/ date[0].wYear = 9999; date[0].wMonth = 12; date[0].wDayOfWeek = 5; date[0].wDay = 31; @@ -272,368 +634,6 @@ namespace DateCalculationUnitTests datetimeSubtractCase[2].dateDiff = dateDifference[1]; // subtract } -/* Duration Between Two Date Tests -- Timediff obtained after calculation should be checked to be identical */ -TEST_METHOD(TestDateDiff) -{ - // TODO - MSFT 10331900, fix this test - - // for (int testIndex = 0; testIndex < c_diffTestCase; testIndex++) - //{ - // DateDifference diff; - // DateUnit dateOutputFormat; - - // switch (testIndex) - // { - // case 0: - // case 2: - // dateOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Day; - // break; - // case 1: - // dateOutputFormat = DateUnit::Day; - // break; - // case 3: - // case 8: - // dateOutputFormat = DateUnit::Week | DateUnit::Day; - // break; - // case 7: - // dateOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Day; - // break; - // case 4: - // case 6: - // dateOutputFormat = DateUnit::Month | DateUnit::Day; - // break; - // case 5: - // dateOutputFormat = DateUnit::Day; - // break; - // } - - // // Calculate the difference - // m_DateCalcEngine->TryGetDateDifference(DateUtils::SystemTimeToDateTime(datetimeDifftest[testIndex].startDate), - // DateUtils::SystemTimeToDateTime(datetimeDifftest[testIndex].endDate), dateOutputFormat, &diff); - - // // Assert for the result - // bool areIdentical = true; - // if (diff.year != datetimeDifftest[testIndex].dateDiff.year || - // diff.month != datetimeDifftest[testIndex].dateDiff.month || - // diff.week != datetimeDifftest[testIndex].dateDiff.week || - // diff.day != datetimeDifftest[testIndex].dateDiff.day) - // { - // areIdentical = false; - // } - - // VERIFY_IS_TRUE(areIdentical); - //} -} - -/*Add Out of bound Tests*/ -TEST_METHOD(TestAddOob) -{ - // TODO - MSFT 10331900, fix this test - - // for (int testIndex = 0; testIndex< c_numAddOobDate; testIndex++) - //{ - // DateTime endDate; - - // // Add Duration - // bool isValid = m_DateCalcEngine->AddDuration(DateUtils::SystemTimeToDateTime(datetimeBoundAdd[testIndex].startDate), - // datetimeBoundAdd[testIndex].dateDiff, &endDate); - - // // Assert for the result - // VERIFY_IS_FALSE(isValid); - //} -} - -/*Subtract Out of bound Tests*/ -TEST_METHOD(TestSubtractOob) -{ - for (int testIndex = 0; testIndex < c_numSubtractOobDate; testIndex++) - { - // Subtract Duration - auto endDate = m_DateCalcEngine->SubtractDuration( - DateUtils::SystemTimeToDateTime(datetimeBoundSubtract[testIndex].startDate), datetimeBoundSubtract[testIndex].dateDiff); - - // Assert for the result - VERIFY_IS_NULL(endDate); - } -} - -// Add Tests -TEST_METHOD(TestAddition) -{ - // TODO - MSFT 10331900, fix this test - - // for (int testIndex = 0; testIndex < c_addCases; testIndex++) - //{ - // DateTime endDate; - - // // Add Duration - // bool isValid = m_DateCalcEngine->AddDuration(DateUtils::SystemTimeToDateTime(datetimeAddCase[testIndex].startDate), - // datetimeAddCase[testIndex].dateDiff, &endDate); - - // // Assert for the result - // VERIFY_IS_TRUE(isValid); - - // SYSTEMTIME systemTime = DateUtils::DateTimeToSystemTime(endDate); - // if (systemTime.wYear != datetimeAddCase[testIndex].endDate.wYear || - // systemTime.wMonth != datetimeAddCase[testIndex].endDate.wMonth || - // systemTime.wDay != datetimeAddCase[testIndex].endDate.wDay || - // systemTime.wDayOfWeek != datetimeAddCase[testIndex].endDate.wDayOfWeek) - // { - // isValid = false; - // } - - // VERIFY_IS_TRUE(isValid); - //} -} - -// Subtract Tests -TEST_METHOD(TestSubtraction) -{ - // TODO - MSFT 10331900, fix this test - - // for (int testIndex = 0; testIndex < c_subtractCases; testIndex++) - //{ - // DateTime endDate; - - // // Subtract Duration - // bool isValid = m_DateCalcEngine->SubtractDuration(DateUtils::SystemTimeToDateTime(datetimeSubtractCase[testIndex].startDate), - // datetimeSubtractCase[testIndex].dateDiff, &endDate); - - // // assert for the result - // VERIFY_IS_TRUE(isValid); - - // SYSTEMTIME systemTime = DateUtils::DateTimeToSystemTime(endDate); - // if (systemTime.wYear != datetimeSubtractCase[testIndex].endDate.wYear || - // systemTime.wMonth != datetimeSubtractCase[testIndex].endDate.wMonth || - // systemTime.wDay != datetimeSubtractCase[testIndex].endDate.wDay || - // systemTime.wDayOfWeek != datetimeSubtractCase[testIndex].endDate.wDayOfWeek) - // { - // isValid = false; - // } - - // VERIFY_IS_TRUE(isValid); - //} -} -}; - -TEST_CLASS(DateCalculatorViewModelTests){ public: TEST_CLASS_INITIALIZE(TestClassSetup){ /* Test Case Data */ - // Dates - DD.MM.YYYY - /*31.12.9999*/ date[0].wYear = 9999; -date[0].wMonth = 12; -date[0].wDayOfWeek = 5; -date[0].wDay = 31; -date[0].wHour = 0; -date[0].wMinute = 0; -date[0].wSecond = 0; -date[0].wMilliseconds = 0; -/*30.12.9999*/ date[1].wYear = 9999; -date[1].wMonth = 12; -date[1].wDayOfWeek = 4; -date[1].wDay = 30; -date[1].wHour = 0; -date[1].wMinute = 0; -date[1].wSecond = 0; -date[1].wMilliseconds = 0; -/*31.12.9998*/ date[2].wYear = 9998; -date[2].wMonth = 12; -date[2].wDayOfWeek = 4; -date[2].wDay = 31; -date[2].wHour = 0; -date[2].wMinute = 0; -date[2].wSecond = 0; -date[2].wMilliseconds = 0; -/*01.01.1601*/ date[3].wYear = 1601; -date[3].wMonth = 1; -date[3].wDayOfWeek = 1; -date[3].wDay = 1; -date[3].wHour = 0; -date[3].wMinute = 0; -date[3].wSecond = 0; -date[3].wMilliseconds = 0; -/*02.01.1601*/ date[4].wYear = 1601; -date[4].wMonth = 1; -date[4].wDayOfWeek = 2; -date[4].wDay = 2; -date[4].wHour = 0; -date[4].wMinute = 0; -date[4].wSecond = 0; -date[4].wMilliseconds = 0; -/*10.05.2008*/ date[5].wYear = 2008; -date[5].wMonth = 5; -date[5].wDayOfWeek = 6; -date[5].wDay = 10; -date[5].wHour = 0; -date[5].wMinute = 0; -date[5].wSecond = 0; -date[5].wMilliseconds = 0; -/*10.03.2008*/ date[6].wYear = 2008; -date[6].wMonth = 3; -date[6].wDayOfWeek = 1; -date[6].wDay = 10; -date[6].wHour = 0; -date[6].wMinute = 0; -date[6].wSecond = 0; -date[6].wMilliseconds = 0; -/*29.02.2008*/ date[7].wYear = 2008; -date[7].wMonth = 2; -date[7].wDayOfWeek = 5; -date[7].wDay = 29; -date[7].wHour = 0; -date[7].wMinute = 0; -date[7].wSecond = 0; -date[7].wMilliseconds = 0; -/*28.02.2007*/ date[8].wYear = 2007; -date[8].wMonth = 2; -date[8].wDayOfWeek = 3; -date[8].wDay = 28; -date[8].wHour = 0; -date[8].wMinute = 0; -date[8].wSecond = 0; -date[8].wMilliseconds = 0; -/*10.03.2007*/ date[9].wYear = 2007; -date[9].wMonth = 3; -date[9].wDayOfWeek = 6; -date[9].wDay = 10; -date[9].wHour = 0; -date[9].wMinute = 0; -date[9].wSecond = 0; -date[9].wMilliseconds = 0; -/*10.05.2007*/ date[10].wYear = 2007; -date[10].wMonth = 5; -date[10].wDayOfWeek = 4; -date[10].wDay = 10; -date[10].wHour = 0; -date[10].wMinute = 0; -date[10].wSecond = 0; -date[10].wMilliseconds = 0; -/*29.01.2008*/ date[11].wYear = 2008; -date[11].wMonth = 1; -date[11].wDayOfWeek = 2; -date[11].wDay = 29; -date[11].wHour = 0; -date[11].wMinute = 0; -date[11].wSecond = 0; -date[11].wMilliseconds = 0; -/*28.01.2007*/ date[12].wYear = 2007; -date[12].wMonth = 1; -date[12].wDayOfWeek = 0; -date[12].wDay = 28; -date[12].wHour = 0; -date[12].wMinute = 0; -date[12].wSecond = 0; -date[12].wMilliseconds = 0; -/*31.01.2008*/ date[13].wYear = 2008; -date[13].wMonth = 1; -date[13].wDayOfWeek = 4; -date[13].wDay = 31; -date[13].wHour = 0; -date[13].wMinute = 0; -date[13].wSecond = 0; -date[13].wMilliseconds = 0; -/*31.03.2008*/ date[14].wYear = 2008; -date[14].wMonth = 3; -date[14].wDayOfWeek = 1; -date[14].wDay = 31; -date[14].wHour = 0; -date[14].wMinute = 0; -date[14].wSecond = 0; -date[14].wMilliseconds = 0; - -// Date Differences -dateDifference[0].year = 1; -dateDifference[0].month = 1; -dateDifference[1].month = 1; -dateDifference[1].day = 10; -dateDifference[2].day = 2; -/*date[2]-[0]*/ dateDifference[3].week = 52; -dateDifference[3].day = 1; -/*date[2]-[0]*/ dateDifference[4].year = 1; -dateDifference[5].day = 365; -dateDifference[6].month = 1; -dateDifference[7].month = 1; -dateDifference[7].day = 2; -dateDifference[8].day = 31; -dateDifference[9].month = 11; -dateDifference[9].day = 1; -dateDifference[10].year = 8398; -dateDifference[10].month = 11; -dateDifference[10].day = 30; -dateDifference[11].year = 2008; -dateDifference[12].year = 7991; -dateDifference[12].month = 11; -dateDifference[13].week = 416998; -dateDifference[13].day = 1; - -/* Test Cases */ - -// Date Difference test cases -datetimeDifftest[0].startDate = date[0]; -datetimeDifftest[0].endDate = date[3]; -datetimeDifftest[0].dateDiff = dateDifference[10]; -datetimeDifftest[1].startDate = date[0]; -datetimeDifftest[1].endDate = date[2]; -datetimeDifftest[1].dateDiff = dateDifference[5]; -datetimeDifftest[2].startDate = date[0]; -datetimeDifftest[2].endDate = date[2]; -datetimeDifftest[2].dateDiff = dateDifference[4]; -datetimeDifftest[3].startDate = date[0]; -datetimeDifftest[3].endDate = date[2]; -datetimeDifftest[3].dateDiff = dateDifference[3]; -datetimeDifftest[4].startDate = date[14]; -datetimeDifftest[4].endDate = date[7]; -datetimeDifftest[4].dateDiff = dateDifference[7]; -datetimeDifftest[5].startDate = date[14]; -datetimeDifftest[5].endDate = date[7]; -datetimeDifftest[5].dateDiff = dateDifference[8]; -datetimeDifftest[6].startDate = date[11]; -datetimeDifftest[6].endDate = date[8]; -datetimeDifftest[6].dateDiff = dateDifference[9]; -datetimeDifftest[7].startDate = date[13]; -datetimeDifftest[7].endDate = date[0]; -datetimeDifftest[7].dateDiff = dateDifference[12]; -datetimeDifftest[8].startDate = date[13]; -datetimeDifftest[8].endDate = date[0]; -datetimeDifftest[8].dateDiff = dateDifference[13]; - -// Date Add Out of Bound test cases (Negative tests) -/*OutofBound*/ datetimeBoundAdd[0].startDate = date[1]; -datetimeBoundAdd[0].endDate = date[0]; -datetimeBoundAdd[0].dateDiff = dateDifference[2]; // on Add date[0] not used -/*OutofBound*/ datetimeBoundAdd[1].startDate = date[2]; -datetimeBoundAdd[1].endDate = date[0]; -datetimeBoundAdd[1].dateDiff = dateDifference[11]; // on Add date[0] not used - -// Date Subtract Out of Bound test cases (Negative tests) -/*OutofBound*/ datetimeBoundSubtract[0].startDate = date[3]; -datetimeBoundSubtract[0].endDate = date[0]; -datetimeBoundSubtract[0].dateDiff = dateDifference[2]; // on subtract date[0] not used -/*OutofBound*/ datetimeBoundSubtract[1].startDate = date[14]; -datetimeBoundSubtract[1].endDate = date[0]; -datetimeBoundSubtract[1].dateDiff = dateDifference[11]; // on subtract date[0] not used - -// Date Add test cases (Positive tests) -datetimeAddCase[0].startDate = date[13]; -datetimeAddCase[0].endDate = date[7]; -datetimeAddCase[0].dateDiff = dateDifference[6]; // add -datetimeAddCase[1].startDate = date[14]; -datetimeAddCase[1].endDate = date[5]; -datetimeAddCase[1].dateDiff = dateDifference[1]; // add -datetimeAddCase[2].startDate = date[13]; -datetimeAddCase[2].endDate = date[6]; -datetimeAddCase[2].dateDiff = dateDifference[1]; // add - -// Date Subtract test cases (Positive tests) -datetimeSubtractCase[0].startDate = date[14]; -datetimeSubtractCase[0].endDate = date[7]; -datetimeSubtractCase[0].dateDiff = dateDifference[6]; // subtract -datetimeSubtractCase[1].startDate = date[6]; -datetimeSubtractCase[1].endDate = date[11]; -datetimeSubtractCase[1].dateDiff = dateDifference[1]; // subtract -datetimeSubtractCase[2].startDate = date[9]; -datetimeSubtractCase[2].endDate = date[12]; -datetimeSubtractCase[2].dateDiff = dateDifference[1]; // subtract -} - TEST_METHOD(DateCalcViewModelInitializationTest) { auto viewModel = ref new DateCalculatorViewModel(); diff --git a/src/CalculatorUnitTests/Module.cpp b/src/CalculatorUnitTests/Module.cpp new file mode 100644 index 000000000..825b62e6f --- /dev/null +++ b/src/CalculatorUnitTests/Module.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace CalculatorUnitTests +{ + BEGIN_TEST_MODULE_ATTRIBUTE() + TEST_MODULE_ATTRIBUTE(L"APPX:CertificateFileName", L"CalculatorUnitTests.cer:TrustedPeople") + END_TEST_MODULE_ATTRIBUTE() + + TEST_MODULE_INITIALIZE(ModuleSetup) + { + } + + TEST_MODULE_CLEANUP(ModuleCleanup) + { + } +} diff --git a/src/CalculatorUnitTests/NavCategoryUnitTests.cpp b/src/CalculatorUnitTests/NavCategoryUnitTests.cpp index cd122f581..170acf6a1 100644 --- a/src/CalculatorUnitTests/NavCategoryUnitTests.cpp +++ b/src/CalculatorUnitTests/NavCategoryUnitTests.cpp @@ -97,7 +97,7 @@ namespace CalculatorUnitTests // Boundary testing VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(ref new Box(-1))); - VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(ref new Box(17))); + VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(ref new Box(18))); } void NavCategoryUnitTests::IsValidViewMode_AllValid() @@ -106,6 +106,10 @@ namespace CalculatorUnitTests VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Scientific)); VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Programmer)); VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Date)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Graphing)); + } VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Currency)); VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Volume)); VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Length)); @@ -125,9 +129,18 @@ namespace CalculatorUnitTests { VERIFY_IS_FALSE(NavCategory::IsValidViewMode(ViewMode::None)); - // There are 17 total options so int 17 should be the first invalid - VERIFY_IS_TRUE(NavCategory::IsValidViewMode(static_cast(16))); - VERIFY_IS_FALSE(NavCategory::IsValidViewMode(static_cast(17))); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + // There are 18 total options so int 18 should be the first invalid + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(static_cast(17))); + VERIFY_IS_FALSE(NavCategory::IsValidViewMode(static_cast(18))); + } + else + { + // There are 17 total options when graphing calculator is not present, so int 17 should be the first invalid + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(static_cast(16))); + VERIFY_IS_FALSE(NavCategory::IsValidViewMode(static_cast(17))); + } // Also verify the lower bound VERIFY_IS_TRUE(NavCategory::IsValidViewMode(static_cast(0))); @@ -141,6 +154,10 @@ namespace CalculatorUnitTests VERIFY_IS_TRUE(NavCategory::IsCalculatorViewMode(ViewMode::Programmer)); VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Date)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Graphing)); + } VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Currency)); VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Volume)); @@ -164,6 +181,10 @@ namespace CalculatorUnitTests VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Programmer)); VERIFY_IS_TRUE(NavCategory::IsDateCalculatorViewMode(ViewMode::Date)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Graphing)); + } VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Currency)); VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Volume)); @@ -185,8 +206,11 @@ namespace CalculatorUnitTests VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Standard)); VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Scientific)); VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Programmer)); - VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Date)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Graphing)); + } VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Currency)); VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Volume)); @@ -209,6 +233,10 @@ namespace CalculatorUnitTests VERIFY_ARE_EQUAL(StringReference(L"Scientific"), NavCategory::GetFriendlyName(ViewMode::Scientific)); VERIFY_ARE_EQUAL(StringReference(L"Programmer"), NavCategory::GetFriendlyName(ViewMode::Programmer)); VERIFY_ARE_EQUAL(StringReference(L"Date"), NavCategory::GetFriendlyName(ViewMode::Date)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_ARE_EQUAL(StringReference(L"Graphing"), NavCategory::GetFriendlyName(ViewMode::Graphing)); + } VERIFY_ARE_EQUAL(StringReference(L"Currency"), NavCategory::GetFriendlyName(ViewMode::Currency)); VERIFY_ARE_EQUAL(StringReference(L"Volume"), NavCategory::GetFriendlyName(ViewMode::Volume)); VERIFY_ARE_EQUAL(StringReference(L"Length"), NavCategory::GetFriendlyName(ViewMode::Length)); @@ -232,6 +260,10 @@ namespace CalculatorUnitTests VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Scientific)); VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Programmer)); VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Date)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Graphing)); + } VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Currency)); VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Volume)); @@ -251,12 +283,23 @@ namespace CalculatorUnitTests void NavCategoryUnitTests::GetIndex() { // Index is the 0-based ordering of modes - vector orderedModes = { ViewMode::Standard, ViewMode::Scientific, ViewMode::Programmer, ViewMode::Date, ViewMode::Currency, - ViewMode::Volume, ViewMode::Length, ViewMode::Weight, ViewMode::Temperature, ViewMode::Energy, - ViewMode::Area, ViewMode::Speed, ViewMode::Time, ViewMode::Power, ViewMode::Data, - ViewMode::Pressure, ViewMode::Angle }; + vector orderedModes; + + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + orderedModes = { ViewMode::Standard, ViewMode::Scientific, ViewMode::Graphing, ViewMode::Programmer, ViewMode::Date, ViewMode::Currency, + ViewMode::Volume, ViewMode::Length, ViewMode::Weight, ViewMode::Temperature, ViewMode::Energy, ViewMode::Area, + ViewMode::Speed, ViewMode::Time, ViewMode::Power, ViewMode::Data, ViewMode::Pressure, ViewMode::Angle }; + } + else + { + orderedModes = { ViewMode::Standard, ViewMode::Scientific, ViewMode::Programmer, ViewMode::Date, ViewMode::Currency, ViewMode::Volume, + ViewMode::Length, ViewMode::Weight, ViewMode::Temperature, ViewMode::Energy, ViewMode::Area, ViewMode::Speed, + ViewMode::Time, ViewMode::Power, ViewMode::Data, ViewMode::Pressure, ViewMode::Angle }; + } - for (size_t index = 0; index < orderedModes.size(); index++) + auto orderedModesSize = size(orderedModes); + for (size_t index = 0; index < orderedModesSize; index++) { ViewMode mode = orderedModes[index]; VERIFY_ARE_EQUAL(index, (size_t)NavCategory::GetIndex(mode)); @@ -268,12 +311,23 @@ namespace CalculatorUnitTests void NavCategoryUnitTests::GetPosition() { // Position is the 1-based ordering of modes - vector orderedModes = { ViewMode::Standard, ViewMode::Scientific, ViewMode::Programmer, ViewMode::Date, ViewMode::Currency, - ViewMode::Volume, ViewMode::Length, ViewMode::Weight, ViewMode::Temperature, ViewMode::Energy, - ViewMode::Area, ViewMode::Speed, ViewMode::Time, ViewMode::Power, ViewMode::Data, - ViewMode::Pressure, ViewMode::Angle }; + vector orderedModes; + + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + orderedModes = { ViewMode::Standard, ViewMode::Scientific, ViewMode::Graphing, ViewMode::Programmer, ViewMode::Date, ViewMode::Currency, + ViewMode::Volume, ViewMode::Length, ViewMode::Weight, ViewMode::Temperature, ViewMode::Energy, ViewMode::Area, + ViewMode::Speed, ViewMode::Time, ViewMode::Power, ViewMode::Data, ViewMode::Pressure, ViewMode::Angle }; + } + else + { + orderedModes = { ViewMode::Standard, ViewMode::Scientific, ViewMode::Programmer, ViewMode::Date, ViewMode::Currency, ViewMode::Volume, + ViewMode::Length, ViewMode::Weight, ViewMode::Temperature, ViewMode::Energy, ViewMode::Area, ViewMode::Speed, + ViewMode::Time, ViewMode::Power, ViewMode::Data, ViewMode::Pressure, ViewMode::Angle }; + } - for (size_t pos = 1; pos <= orderedModes.size(); pos++) + auto orderedModesSize = size(orderedModes); + for (size_t pos = 1; pos <= orderedModesSize; pos++) { ViewMode mode = orderedModes[pos - 1]; VERIFY_ARE_EQUAL(pos, (size_t)NavCategory::GetPosition(mode)); @@ -295,9 +349,17 @@ namespace CalculatorUnitTests { VERIFY_ARE_EQUAL(0, NavCategory::GetIndexInGroup(ViewMode::Standard, CategoryGroupType::Calculator)); VERIFY_ARE_EQUAL(1, NavCategory::GetIndexInGroup(ViewMode::Scientific, CategoryGroupType::Calculator)); - VERIFY_ARE_EQUAL(2, NavCategory::GetIndexInGroup(ViewMode::Programmer, CategoryGroupType::Calculator)); - VERIFY_ARE_EQUAL(3, NavCategory::GetIndexInGroup(ViewMode::Date, CategoryGroupType::Calculator)); - + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_ARE_EQUAL(2, NavCategory::GetIndexInGroup(ViewMode::Graphing, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(3, NavCategory::GetIndexInGroup(ViewMode::Programmer, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(4, NavCategory::GetIndexInGroup(ViewMode::Date, CategoryGroupType::Calculator)); + } + else + { + VERIFY_ARE_EQUAL(2, NavCategory::GetIndexInGroup(ViewMode::Programmer, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(3, NavCategory::GetIndexInGroup(ViewMode::Date, CategoryGroupType::Calculator)); + } VERIFY_ARE_EQUAL(0, NavCategory::GetIndexInGroup(ViewMode::Currency, CategoryGroupType::Converter)); VERIFY_ARE_EQUAL(1, NavCategory::GetIndexInGroup(ViewMode::Volume, CategoryGroupType::Converter)); VERIFY_ARE_EQUAL(2, NavCategory::GetIndexInGroup(ViewMode::Length, CategoryGroupType::Converter)); @@ -320,7 +382,17 @@ namespace CalculatorUnitTests { VERIFY_ARE_EQUAL(ViewMode::Standard, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number1)); VERIFY_ARE_EQUAL(ViewMode::Scientific, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number2)); - VERIFY_ARE_EQUAL(ViewMode::Programmer, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number3)); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + VERIFY_ARE_EQUAL(ViewMode::Graphing, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number3)); + VERIFY_ARE_EQUAL(ViewMode::Programmer, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number4)); + VERIFY_ARE_EQUAL(ViewMode::Date, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number5)); + } + else + { + VERIFY_ARE_EQUAL(ViewMode::Programmer, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number3)); + VERIFY_ARE_EQUAL(ViewMode::Date, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number4)); + } } TEST_CLASS(NavCategoryGroupUnitTests) @@ -329,14 +401,13 @@ namespace CalculatorUnitTests TEST_METHOD(CreateNavCategoryGroup); private: - void ValidateNavCategory(IObservableVector ^ categories, unsigned int index, ViewMode expectedMode, int expectedPosition) + void ValidateNavCategory(IObservableVector ^ categories, unsigned int index, ViewMode expectedMode) { VERIFY_IS_LESS_THAN(0u, categories->Size); VERIFY_IS_GREATER_THAN(categories->Size, index); NavCategory ^ category = categories->GetAt(index); VERIFY_ARE_EQUAL(expectedMode, category->Mode); - VERIFY_ARE_EQUAL(expectedPosition, category->Position); } }; @@ -350,29 +421,39 @@ namespace CalculatorUnitTests VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, calculatorGroup->GroupType); IObservableVector ^ calculatorCategories = calculatorGroup->Categories; - VERIFY_ARE_EQUAL(4, calculatorCategories->Size); - ValidateNavCategory(calculatorCategories, 0u, ViewMode::Standard, 1); - ValidateNavCategory(calculatorCategories, 1u, ViewMode::Scientific, 2); - ValidateNavCategory(calculatorCategories, 2u, ViewMode::Programmer, 3); - ValidateNavCategory(calculatorCategories, 3u, ViewMode::Date, 4); + ValidateNavCategory(calculatorCategories, 0u, ViewMode::Standard); + ValidateNavCategory(calculatorCategories, 1u, ViewMode::Scientific); + if (Windows::Foundation::Metadata::ApiInformation::IsMethodPresent("Windows.UI.Text.RichEditTextDocument", "GetMath")) + { + ValidateNavCategory(calculatorCategories, 2u, ViewMode::Graphing); + ValidateNavCategory(calculatorCategories, 3u, ViewMode::Programmer); + ValidateNavCategory(calculatorCategories, 4u, ViewMode::Date); + VERIFY_ARE_EQUAL(5, calculatorCategories->Size); + } + else + { + ValidateNavCategory(calculatorCategories, 2u, ViewMode::Programmer); + ValidateNavCategory(calculatorCategories, 3u, ViewMode::Date); + VERIFY_ARE_EQUAL(4, calculatorCategories->Size); + } NavCategoryGroup ^ converterGroup = menuOptions->GetAt(1); VERIFY_ARE_EQUAL(CategoryGroupType::Converter, converterGroup->GroupType); IObservableVector ^ converterCategories = converterGroup->Categories; VERIFY_ARE_EQUAL(13, converterCategories->Size); - ValidateNavCategory(converterCategories, 0u, ViewMode::Currency, 5); - ValidateNavCategory(converterCategories, 1u, ViewMode::Volume, 6); - ValidateNavCategory(converterCategories, 2u, ViewMode::Length, 7); - ValidateNavCategory(converterCategories, 3u, ViewMode::Weight, 8); - ValidateNavCategory(converterCategories, 4u, ViewMode::Temperature, 9); - ValidateNavCategory(converterCategories, 5u, ViewMode::Energy, 10); - ValidateNavCategory(converterCategories, 6u, ViewMode::Area, 11); - ValidateNavCategory(converterCategories, 7u, ViewMode::Speed, 12); - ValidateNavCategory(converterCategories, 8u, ViewMode::Time, 13); - ValidateNavCategory(converterCategories, 9u, ViewMode::Power, 14); - ValidateNavCategory(converterCategories, 10u, ViewMode::Data, 15); - ValidateNavCategory(converterCategories, 11u, ViewMode::Pressure, 16); - ValidateNavCategory(converterCategories, 12u, ViewMode::Angle, 17); + ValidateNavCategory(converterCategories, 0u, ViewMode::Currency); + ValidateNavCategory(converterCategories, 1u, ViewMode::Volume); + ValidateNavCategory(converterCategories, 2u, ViewMode::Length); + ValidateNavCategory(converterCategories, 3u, ViewMode::Weight); + ValidateNavCategory(converterCategories, 4u, ViewMode::Temperature); + ValidateNavCategory(converterCategories, 5u, ViewMode::Energy); + ValidateNavCategory(converterCategories, 6u, ViewMode::Area); + ValidateNavCategory(converterCategories, 7u, ViewMode::Speed); + ValidateNavCategory(converterCategories, 8u, ViewMode::Time); + ValidateNavCategory(converterCategories, 9u, ViewMode::Power); + ValidateNavCategory(converterCategories, 10u, ViewMode::Data); + ValidateNavCategory(converterCategories, 11u, ViewMode::Pressure); + ValidateNavCategory(converterCategories, 12u, ViewMode::Angle); } } diff --git a/src/CalculatorUnitTests/resource.h b/src/CalculatorUnitTests/resource.h new file mode 100644 index 000000000..12d93a32c --- /dev/null +++ b/src/CalculatorUnitTests/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by CalculatorUnitTests_VS.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/GraphControl/Control/Grapher.cpp b/src/GraphControl/Control/Grapher.cpp new file mode 100644 index 000000000..f6953d912 --- /dev/null +++ b/src/GraphControl/Control/Grapher.cpp @@ -0,0 +1,906 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Grapher.h" +#include "IBitmap.h" +#include "../../CalcViewModel/GraphingCalculatorEnums.h" + +using namespace Graphing; +using namespace GraphControl; +using namespace GraphControl::DX; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::Devices::Input; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Storage::Streams; +using namespace Windows::System; +using namespace Windows::UI; +using namespace Windows::UI::Core; +using namespace Windows::UI::Input; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace GraphControl; + +DEPENDENCY_PROPERTY_INITIALIZATION(Grapher, ForceProportionalAxes); +DEPENDENCY_PROPERTY_INITIALIZATION(Grapher, Variables); +DEPENDENCY_PROPERTY_INITIALIZATION(Grapher, Equations); +DEPENDENCY_PROPERTY_INITIALIZATION(Grapher, AxesColor); +DEPENDENCY_PROPERTY_INITIALIZATION(Grapher, GraphBackground); + +namespace +{ + constexpr auto s_defaultStyleKey = L"GraphControl.Grapher"; + constexpr auto s_templateKey_SwapChainPanel = L"GraphSurface"; + + constexpr auto s_X = L"x"; + constexpr auto s_Y = L"y"; + constexpr auto s_defaultFormatType = FormatType::MathML; + constexpr auto s_getGraphOpeningTags = L"show2d"; + constexpr auto s_getGraphClosingTags = L""; + + // Helper function for converting a pointer position to a position that the graphing engine will understand. + // posX/posY are the pointer position elements and width,height are the dimensions of the graph surface. + // The graphing engine interprets x,y position between the range [-1, 1]. + // Translate the pointer position to the [-1, 1] bounds. + __inline pair PointerPositionToGraphPosition(double posX, double posY, double width, double height) + { + return make_pair((2 * posX / width - 1), (1 - 2 * posY / height)); + } +} + +namespace GraphControl +{ + Grapher::Grapher() + : m_solver{ IMathSolver::CreateMathSolver() } + , m_graph{ m_solver->CreateGrapher() } + , m_Moving{ false } + { + Equations = ref new EquationCollection(); + + m_solver->ParsingOptions().SetFormatType(s_defaultFormatType); + m_solver->FormatOptions().SetFormatType(s_defaultFormatType); + m_solver->FormatOptions().SetMathMLPrefix(wstring(L"mml")); + + DefaultStyleKey = StringReference(s_defaultStyleKey); + + this->ManipulationMode = ManipulationModes::TranslateX | ManipulationModes::TranslateY | ManipulationModes::TranslateInertia | ManipulationModes::Scale + | ManipulationModes::ScaleInertia; + + auto cw = CoreWindow::GetForCurrentThread(); + cw->KeyDown += ref new TypedEventHandler(this, &Grapher::OnCoreKeyDown); + cw->KeyUp += ref new TypedEventHandler(this, &Grapher::OnCoreKeyUp); + + auto& formatOptions = m_solver->FormatOptions(); + } + + void Grapher::ZoomFromCenter(double scale) + { + ScaleRange(0, 0, scale); + } + + void Grapher::ScaleRange(double centerX, double centerY, double scale) + { + if (m_graph != nullptr && m_renderMain != nullptr) + { + if (auto renderer = m_graph->GetRenderer()) + { + if (SUCCEEDED(renderer->ScaleRange(centerX, centerY, scale))) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + void Grapher::ResetGrid() + { + if (m_graph != nullptr && m_renderMain != nullptr) + { + if (auto renderer = m_graph->GetRenderer()) + { + if (SUCCEEDED(renderer->ResetRange())) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + void Grapher::OnApplyTemplate() + { + auto swapChainPanel = dynamic_cast(GetTemplateChild(StringReference(s_templateKey_SwapChainPanel))); + if (swapChainPanel) + { + swapChainPanel->AllowFocusOnInteraction = true; + m_renderMain = ref new RenderMain(swapChainPanel); + m_renderMain->BackgroundColor = GraphBackground; + } + + TryUpdateGraph(false); + } + + void Grapher::OnEquationsPropertyChanged(EquationCollection ^ oldValue, EquationCollection ^ newValue) + { + if (oldValue != nullptr) + { + if (m_tokenEquationChanged.Value != 0) + { + oldValue->EquationChanged -= m_tokenEquationChanged; + m_tokenEquationChanged.Value = 0; + } + + if (m_tokenEquationStyleChanged.Value != 0) + { + oldValue->EquationStyleChanged -= m_tokenEquationStyleChanged; + m_tokenEquationStyleChanged.Value = 0; + } + + if (m_tokenEquationLineEnabledChanged.Value != 0) + { + oldValue->EquationLineEnabledChanged -= m_tokenEquationLineEnabledChanged; + m_tokenEquationLineEnabledChanged.Value = 0; + } + } + + if (newValue != nullptr) + { + m_tokenEquationChanged = newValue->EquationChanged += ref new EquationChangedEventHandler(this, &Grapher::OnEquationChanged); + + m_tokenEquationStyleChanged = newValue->EquationStyleChanged += ref new EquationChangedEventHandler(this, &Grapher::OnEquationStyleChanged); + + m_tokenEquationLineEnabledChanged = newValue->EquationLineEnabledChanged += + ref new EquationChangedEventHandler(this, &Grapher::OnEquationLineEnabledChanged); + } + + PlotGraph(false); + } + + void Grapher::OnEquationChanged(Equation ^ equation) + { + // If the equation was previously valid, we should try to graph again in the event of the failure + bool shouldRetry = equation->IsValidated; + + // Reset these properties if the equation is requesting to be graphed again + equation->HasGraphError = false; + equation->IsValidated = false; + + TryPlotGraph(false, shouldRetry); + } + + void Grapher::OnEquationStyleChanged(Equation ^) + { + if (m_graph) + { + UpdateGraphOptions(m_graph->GetOptions(), GetGraphableEquations()); + } + + if (m_renderMain) + { + m_renderMain->RunRenderPass(); + } + } + + void Grapher::OnEquationLineEnabledChanged(Equation ^ equation) + { + // If the equation is in an error state or is empty, it should not be graphed anyway. + if (equation->HasGraphError || equation->Expression->IsEmpty()) + { + return; + } + + PlotGraph(true); + } + + KeyGraphFeaturesInfo ^ Grapher::AnalyzeEquation(Equation ^ equation) + { + auto result = ref new KeyGraphFeaturesInfo(); + if (auto graph = GetGraph(equation)) + { + if (auto analyzer = graph->GetAnalyzer()) + { + if (analyzer->CanFunctionAnalysisBePerformed()) + { + if (S_OK + == analyzer->PerformFunctionAnalysis( + (Graphing::Analyzer::NativeAnalysisType)Graphing::Analyzer::PerformAnalysisType::PerformAnalysisType_All)) + { + Graphing::IGraphFunctionAnalysisData functionAnalysisData = m_solver->Analyze(analyzer.get()); + return KeyGraphFeaturesInfo::Create(functionAnalysisData); + } + } + else + { + return KeyGraphFeaturesInfo::Create(CalculatorApp::AnalysisErrorType::AnalysisNotSupported); + } + } + } + + return KeyGraphFeaturesInfo::Create(CalculatorApp::AnalysisErrorType::AnalysisCouldNotBePerformed); + } + + void Grapher::PlotGraph(bool keepCurrentView) + { + TryPlotGraph(keepCurrentView, false); + } + + void Grapher::TryPlotGraph(bool keepCurrentView, bool shouldRetry) + { + if (TryUpdateGraph(keepCurrentView)) + { + SetEquationsAsValid(); + } + else + { + SetEquationErrors(); + + // If we failed to plot the graph, try again after the bad equations are flagged. + if (shouldRetry) + { + TryUpdateGraph(keepCurrentView); + } + } + } + + bool Grapher::TryUpdateGraph(bool keepCurrentView) + { + optional>> initResult = nullopt; + bool successful = false; + + if (m_renderMain && m_graph != nullptr) + { + unique_ptr graphExpression; + wstring request; + + auto validEqs = GetGraphableEquations(); + + // Will be set to true if the previous graph should be kept in the event of an error + bool shouldKeepPreviousGraph = false; + + if (!validEqs.empty()) + { + wstringstream ss{}; + ss << s_getGraphOpeningTags; + + int numValidEquations = 0; + for (Equation ^ eq : validEqs) + { + if (eq->IsValidated) + { + shouldKeepPreviousGraph = true; + } + + if (numValidEquations++ > 0) + { + ss << L","; + } + + ss << eq->GetRequest()->Data(); + } + + ss << s_getGraphClosingTags; + + request = ss.str(); + } + + if (graphExpression = m_solver->ParseInput(request)) + { + initResult = TryInitializeGraph(keepCurrentView, graphExpression.get()); + + if (initResult != nullopt) + { + UpdateGraphOptions(m_graph->GetOptions(), validEqs); + SetGraphArgs(); + + m_renderMain->Graph = m_graph; + + // It is possible that the render fails, in that case fall through to explicit empty initialization + if (m_renderMain->RunRenderPass()) + { + UpdateVariables(); + successful = true; + } + else + { + // If we failed to render then we have already lost the previous graph + shouldKeepPreviousGraph = false; + initResult = nullopt; + } + } + } + + if (initResult == nullopt) + { + // Do not re-initialize the graph to empty if there are still valid equations graphed + if (!shouldKeepPreviousGraph) + { + initResult = TryInitializeGraph(keepCurrentView, graphExpression.get()); + if (initResult != nullopt) + { + UpdateGraphOptions(m_graph->GetOptions(), validEqs); + SetGraphArgs(); + + m_renderMain->Graph = m_graph; + m_renderMain->RunRenderPass(); + + UpdateVariables(); + + // Initializing an empty graph is only a success if there were no equations to graph. + successful = (validEqs.size() == 0); + } + } + } + } + + // Return true if we were able to graph and render all graphable equations + return successful; + } + + void Grapher::SetEquationsAsValid() + { + for (Equation ^ eq : GetGraphableEquations()) + { + eq->IsValidated = true; + } + } + + void Grapher::SetEquationErrors() + { + for (Equation ^ eq : GetGraphableEquations()) + { + if (!eq->IsValidated) + { + eq->HasGraphError = true; + } + } + } + + void Grapher::SetGraphArgs() + { + if (m_graph) + { + for (auto variable : Variables) + { + m_graph->SetArgValue(variable->Key->Data(), variable->Value); + } + } + } + + shared_ptr Grapher::GetGraph(Equation ^ equation) + { + shared_ptr graph = m_solver->CreateGrapher(); + + wstringstream ss{}; + ss << s_getGraphOpeningTags; + ss << equation->GetRequest()->Data(); + ss << s_getGraphClosingTags; + + wstring request = ss.str(); + unique_ptr graphExpression; + if (graphExpression = m_solver->ParseInput(request)) + { + if (graph->TryInitialize(graphExpression.get())) + { + return graph; + } + } + + return nullptr; + } + + void Grapher::UpdateVariables() + { + auto updatedVariables = ref new Map(); + + if (m_graph) + { + auto graphVariables = m_graph->GetVariables(); + + for (auto graphVar : graphVariables) + { + if (graphVar->GetVariableName() != s_X && graphVar->GetVariableName() != s_Y) + { + auto key = ref new String(graphVar->GetVariableName().data()); + double value = 1.0; + + if (Variables->HasKey(key)) + { + value = Variables->Lookup(key); + } + + updatedVariables->Insert(key, value); + } + } + } + + Variables = updatedVariables; + VariablesUpdated(this, Variables); + } + + void Grapher::SetVariable(Platform::String ^ variableName, double newValue) + { + if (Variables->HasKey(variableName)) + { + if (Variables->Lookup(variableName) == newValue) + { + return; + } + + Variables->Remove(variableName); + } + + Variables->Insert(variableName, newValue); + + if (m_graph) + { + m_graph->SetArgValue(variableName->Data(), newValue); + + if (m_renderMain) + { + m_renderMain->RunRenderPass(); + } + } + } + + void Grapher::UpdateGraphOptions(IGraphingOptions& options, const vector& validEqs) + { + options.SetForceProportional(ForceProportionalAxes); + + if (!options.GetAllowKeyGraphFeaturesForFunctionsWithParameters()) + { + options.SetAllowKeyGraphFeaturesForFunctionsWithParameters(true); + } + + if (!validEqs.empty()) + { + vector graphColors; + graphColors.reserve(validEqs.size()); + for (Equation ^ eq : validEqs) + { + auto lineColor = eq->LineColor; + graphColors.emplace_back(lineColor.R, lineColor.G, lineColor.B, lineColor.A); + } + options.SetGraphColors(graphColors); + } + } + + vector Grapher::GetGraphableEquations() + { + vector validEqs; + + for (Equation ^ eq : Equations) + { + if (eq->IsGraphableEquation()) + { + validEqs.push_back(eq); + } + } + + return validEqs; + } + + void Grapher::OnForceProportionalAxesPropertyChanged(bool /*oldValue*/, bool newValue) + { + m_calculatedForceProportional = newValue; + TryUpdateGraph(false); + } + + void Grapher::OnPointerEntered(PointerRoutedEventArgs ^ e) + { + if (m_renderMain) + { + OnPointerMoved(e); + m_renderMain->DrawNearestPoint = true; + + e->Handled = true; + } + } + + void Grapher::UpdateTracingChanged() + { + if (m_renderMain->Tracing) + { + TracingChangedEvent(true); + TracingValueChangedEvent(m_renderMain->TraceValue); + } + else + { + TracingChangedEvent(false); + } + } + + void Grapher::OnPointerMoved(PointerRoutedEventArgs ^ e) + { + if (m_renderMain) + { + PointerPoint ^ currPoint = e->GetCurrentPoint(/* relativeTo */ this); + m_renderMain->PointerLocation = currPoint->Position; + UpdateTracingChanged(); + + e->Handled = true; + } + } + + void Grapher::OnPointerExited(PointerRoutedEventArgs ^ e) + { + if (m_renderMain) + { + m_renderMain->DrawNearestPoint = false; + TracingChangedEvent(false); + e->Handled = true; + } + } + + void Grapher::OnPointerWheelChanged(PointerRoutedEventArgs ^ e) + { + PointerPoint ^ currentPointer = e->GetCurrentPoint(/*relative to*/ this); + + double delta = currentPointer->Properties->MouseWheelDelta; + + // The maximum delta is 120 according to: + // https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpointproperties.mousewheeldelta#Windows_UI_Input_PointerPointProperties_MouseWheelDelta + // Apply a dampening effect so that small mouse movements have a smoother zoom. + constexpr double scrollDamper = 0.15; + double scale = 1.0 + (abs(delta) / WHEEL_DELTA) * scrollDamper; + + // positive delta if wheel scrolled away from the user + if (delta >= 0) + { + scale = 1.0 / scale; + } + + // For scaling, the graphing engine interprets x,y position between the range [-1, 1]. + // Translate the pointer position to the [-1, 1] bounds. + const auto& pos = currentPointer->Position; + const auto [centerX, centerY] = PointerPositionToGraphPosition(pos.X, pos.Y, ActualWidth, ActualHeight); + + ScaleRange(centerX, centerY, scale); + + e->Handled = true; + } + + void Grapher::OnPointerPressed(PointerRoutedEventArgs ^ e) + { + // Set the pointer capture to the element being interacted with so that only it + // will fire pointer-related events + CapturePointer(e->Pointer); + } + + void Grapher::OnPointerReleased(PointerRoutedEventArgs ^ e) + { + ReleasePointerCapture(e->Pointer); + } + + void Grapher::OnPointerCanceled(PointerRoutedEventArgs ^ e) + { + ReleasePointerCapture(e->Pointer); + } + + void Grapher::OnManipulationDelta(ManipulationDeltaRoutedEventArgs ^ e) + { + if (m_renderMain != nullptr && m_graph != nullptr) + { + if (auto renderer = m_graph->GetRenderer()) + { + // Only call for a render pass if we actually scaled or translated. + bool needsRenderPass = false; + + const double width = ActualWidth; + const double height = ActualHeight; + + const auto& translation = e->Delta.Translation; + double translationX = translation.X; + double translationY = translation.Y; + if (translationX != 0 || translationY != 0) + { + // The graphing engine pans the graph according to a ratio for x and y. + // A value of +1 means move a half screen in the positive direction for the given axis. + // Convert the manipulation's translation values to ratios for the engine. + translationX /= -width; + translationY /= height; + + if (FAILED(renderer->MoveRangeByRatio(translationX, translationY))) + { + return; + } + + needsRenderPass = true; + } + + if (double scale = e->Delta.Scale; scale != 1.0) + { + // The graphing engine interprets scale amounts as the inverse of the value retrieved + // from the ManipulationUpdatedEventArgs. Invert the scale amount for the engine. + scale = 1.0 / scale; + + // Convert from PointerPosition to graph position. + const auto& pos = e->Position; + const auto [centerX, centerY] = PointerPositionToGraphPosition(pos.X, pos.Y, width, height); + + if (FAILED(renderer->ScaleRange(centerX, centerY, scale))) + { + return; + } + + needsRenderPass = true; + } + + if (needsRenderPass) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + RandomAccessStreamReference ^ Grapher::GetGraphBitmapStream() + { + RandomAccessStreamReference ^ outputStream; + + if (m_renderMain != nullptr && m_graph != nullptr) + { + if (auto renderer = m_graph->GetRenderer()) + { + shared_ptr BitmapOut; + bool hasSomeMissingDataOut = false; + HRESULT hr = E_FAIL; + hr = renderer->GetBitmap(BitmapOut, hasSomeMissingDataOut); + if (SUCCEEDED(hr)) + { + // Get the raw date + vector byteVector = BitmapOut->GetData(); + auto arr = ref new Array(&byteVector[0], (unsigned int)byteVector.size()); + + // create a memory stream wrapper + InMemoryRandomAccessStream ^ stream = ref new InMemoryRandomAccessStream(); + + // Get a writer to transfer the data + auto writer = ref new DataWriter(stream->GetOutputStreamAt(0)); + + // write the data + writer->WriteBytes(arr); + writer->StoreAsync()->GetResults(); + + // Get a reference stream to return; + outputStream = RandomAccessStreamReference::CreateFromStream(stream); + } + else + { + OutputDebugString(L"Grapher::GetGraphBitmapStream() unable to get graph image from renderer\r\n"); + winrt::throw_hresult(hr); + } + } + } + + return outputStream; + } +} + +void Grapher::OnCoreKeyUp(CoreWindow ^ sender, KeyEventArgs ^ e) +{ + // We don't want to react to keyboard input unless the graph control has the focus. + // NOTE: you can't select the graph control from the mouse for focus but you can tab to it. + GraphControl::Grapher ^ gcHasFocus = dynamic_cast(FocusManager::GetFocusedElement()); + if (gcHasFocus == nullptr || gcHasFocus != this) + { + // Not a graphingCalculator control so we don't want the input. + return; + } + + switch (e->VirtualKey) + { + case VirtualKey::Left: + case VirtualKey::Right: + case VirtualKey::Down: + case VirtualKey::Up: + case VirtualKey::Shift: + { + HandleKey(false, e->VirtualKey); + } + break; + } +} + +void Grapher::OnCoreKeyDown(CoreWindow ^ sender, KeyEventArgs ^ e) +{ + // We don't want to react to any keys when we are not in the graph control + GraphControl::Grapher ^ gcHasFocus = dynamic_cast(FocusManager::GetFocusedElement()); + if (gcHasFocus == nullptr || gcHasFocus != this) + { + // Not a graphingCalculator control so we don't want the input. + return; + } + + switch (e->VirtualKey) + { + case VirtualKey::Left: + case VirtualKey::Right: + case VirtualKey::Down: + case VirtualKey::Up: + case VirtualKey::Shift: + { + HandleKey(true, e->VirtualKey); + } + break; + } +} + +void Grapher::HandleKey(bool keyDown, VirtualKey key) +{ + int pressedKeys = 0; + if (key == VirtualKey::Left) + { + m_KeysPressed[KeysPressedSlots::Left] = keyDown; + if (keyDown) + { + pressedKeys++; + } + } + if (key == VirtualKey::Right) + { + m_KeysPressed[KeysPressedSlots::Right] = keyDown; + if (keyDown) + { + pressedKeys++; + } + } + if (key == VirtualKey::Up) + { + m_KeysPressed[KeysPressedSlots::Up] = keyDown; + if (keyDown) + { + pressedKeys++; + } + } + if (key == VirtualKey::Down) + { + m_KeysPressed[KeysPressedSlots::Down] = keyDown; + if (keyDown) + { + pressedKeys++; + } + } + if (key == VirtualKey::Shift) + { + m_KeysPressed[KeysPressedSlots::Accelerator] = keyDown; + } + + if (pressedKeys > 0 && !m_Moving) + { + m_Moving = true; + // Key(s) we care about, so ensure we are ticking our timer (and that we have one to tick) + if (m_TracingTrackingTimer == nullptr) + { + m_TracingTrackingTimer = ref new DispatcherTimer(); + + m_TracingTrackingTimer->Tick += ref new EventHandler(this, &Grapher::HandleTracingMovementTick); + TimeSpan ts; + ts.Duration = 100000; // .1 second + m_TracingTrackingTimer->Interval = ts; + auto i = m_TracingTrackingTimer->Interval; + } + m_TracingTrackingTimer->Start(); + } +} + +void Grapher::HandleTracingMovementTick(Object ^ sender, Object ^ e) +{ + int delta = 5; + int liveKeys = 0; + + if (m_KeysPressed[KeysPressedSlots::Accelerator]) + { + delta = 1; + } + + auto curPos = ActiveTraceCursorPosition; + + if (m_KeysPressed[KeysPressedSlots::Left]) + { + liveKeys++; + curPos.X -= delta; + if (curPos.X < 0) + { + curPos.X = 0; + } + } + + if (m_KeysPressed[KeysPressedSlots::Right]) + { + liveKeys++; + curPos.X += delta; + if (curPos.X > ActualWidth - delta) + { + curPos.X = (float)ActualWidth - delta; // TODO change this to deal with size of cursor + } + } + + if (m_KeysPressed[KeysPressedSlots::Up]) + { + liveKeys++; + curPos.Y -= delta; + if (curPos.Y < 0) + { + curPos.Y = 0; + } + } + + if (m_KeysPressed[KeysPressedSlots::Down]) + { + liveKeys++; + curPos.Y += delta; + if (curPos.Y > ActualHeight - delta) + { + curPos.Y = (float)ActualHeight - delta; // TODO change this to deal with size of cursor + } + } + + if (liveKeys == 0) + { + m_Moving = false; + + // Non of the keys we care about are being hit any longer so shut down our timer + m_TracingTrackingTimer->Stop(); + } + else + { + ActiveTraceCursorPosition = curPos; + PointerValueChangedEvent(curPos); + } +} + +String ^ Grapher::ConvertToLinear(String ^ mmlString) +{ + m_solver->FormatOptions().SetFormatType(FormatType::LinearInput); + + auto expression = m_solver->ParseInput(mmlString->Data()); + auto linearExpression = m_solver->Serialize(expression.get()); + + m_solver->FormatOptions().SetFormatType(s_defaultFormatType); + + return ref new String(linearExpression.c_str()); +} + +void Grapher::OnAxesColorPropertyChanged(Windows::UI::Color /*oldValue*/, Windows::UI::Color newValue) +{ + if (m_graph) + { + auto axesColor = Graphing::Color(newValue.R, newValue.G, newValue.B, newValue.A); + m_graph->GetOptions().SetAxisColor(axesColor); + m_graph->GetOptions().SetFontColor(axesColor); + } +} + +void Grapher::OnGraphBackgroundPropertyChanged(Windows::UI::Color /*oldValue*/, Windows::UI::Color newValue) +{ + if (m_renderMain) + { + m_renderMain->BackgroundColor = newValue; + } + if (m_graph) + { + auto color = Graphing::Color(newValue.R, newValue.G, newValue.B, newValue.A); + m_graph->GetOptions().SetBackColor(color); + m_graph->GetOptions().SetBoxColor(color); + } +} + +optional>> Grapher::TryInitializeGraph(bool keepCurrentView, const IExpression* graphingExp) +{ + if (keepCurrentView) + { + double xMin, xMax, yMin, yMax; + m_graph->GetRenderer()->GetDisplayRanges(xMin, xMax, yMin, yMax); + auto initResult = m_graph->TryInitialize(graphingExp); + m_graph->GetRenderer()->SetDisplayRanges(xMin, xMax, yMin, yMax); + return initResult; + } + else + { + return m_graph->TryInitialize(graphingExp); + } +} diff --git a/src/GraphControl/Control/Grapher.h b/src/GraphControl/Control/Grapher.h new file mode 100644 index 000000000..2b6338333 --- /dev/null +++ b/src/GraphControl/Control/Grapher.h @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "DirectX/RenderMain.h" +#include "../Models/Equation.h" +#include "../Models/EquationCollection.h" +#include "../Utils.h" +#include "IGraphAnalyzer.h" +#include "IMathSolver.h" +#include "Common.h" +#include "Models/KeyGraphFeaturesInfo.h" +#include + +namespace GraphControl +{ +public + delegate void TracingChangedEventHandler(bool newValue); + +public + delegate void TracingValueChangedEventHandler(Windows::Foundation::Point value); +public + delegate void PointerValueChangedEventHandler(Windows::Foundation::Point value); + + [Windows::UI::Xaml::Markup::ContentPropertyAttribute(Name = L"Equations")] public ref class Grapher sealed + : public Windows::UI::Xaml::Controls::Control, + public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + event TracingValueChangedEventHandler ^ TracingValueChangedEvent; + event PointerValueChangedEventHandler ^ PointerValueChangedEvent; + event TracingChangedEventHandler ^ TracingChangedEvent; + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler ^ PropertyChanged; + + public: + Grapher(); + + DEPENDENCY_PROPERTY_OWNER(Grapher); + DEPENDENCY_PROPERTY_WITH_DEFAULT_AND_CALLBACK(bool, ForceProportionalAxes, true); + DEPENDENCY_PROPERTY_WITH_DEFAULT( + SINGLE_ARG(Windows::Foundation::Collections::IObservableMap ^), + Variables, + SINGLE_ARG(ref new Platform::Collections::Map())); + DEPENDENCY_PROPERTY_R_WITH_DEFAULT_AND_CALLBACK(GraphControl::EquationCollection ^, Equations, nullptr); + DEPENDENCY_PROPERTY_WITH_DEFAULT_AND_CALLBACK(Windows::UI::Color, AxesColor, Windows::UI::Colors::Transparent); + DEPENDENCY_PROPERTY_WITH_DEFAULT_AND_CALLBACK(Windows::UI::Color, GraphBackground, Windows::UI::Colors::Transparent); + + // Pass active tracing turned on or off down to the renderer + property bool ActiveTracing + { + bool get() + { + return m_renderMain != nullptr && m_renderMain->ActiveTracing; + } + + void set(bool value) + { + if (m_renderMain != nullptr && m_renderMain->ActiveTracing != value) + { + m_renderMain->ActiveTracing = value; + UpdateTracingChanged(); + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(L"ActiveTracing")); + } + } + } + + void ZoomFromCenter(double scale); + void ResetGrid(); + + property Windows::Foundation::Point TraceValue + { + Windows::Foundation::Point get() + { + return m_renderMain->TraceValue; + } + } + + property Windows::Foundation::Point TraceLocation + { + Windows::Foundation::Point get() + { + return m_renderMain->TraceLocation; + } + } + + property Windows::Foundation::Point ActiveTraceCursorPosition + { + Windows::Foundation::Point get() + { + return m_renderMain->ActiveTraceCursorPosition; + } + + void set(Windows::Foundation::Point newValue) + { + if (m_renderMain->ActiveTraceCursorPosition != newValue) + { + m_renderMain->ActiveTraceCursorPosition = newValue; + UpdateTracingChanged(); + } + } + } + + event Windows::Foundation::EventHandler ^> ^ VariablesUpdated; + void SetVariable(Platform::String ^ variableName, double newValue); + Platform::String ^ ConvertToLinear(Platform::String ^ mmlString); + + /// + /// Draw the graph. Call this method if you add or modify an equation. + /// + /// Force the graph control to not pan or zoom to adapt the view. + void PlotGraph(bool keepCurrentView); + + GraphControl::KeyGraphFeaturesInfo ^ AnalyzeEquation(GraphControl::Equation ^ equation); + + // We can't use the EvalTrigUnitMode enum directly in as the property type because it comes from another module which doesn't expose + // it as a public enum class. So the compiler doesn't recognize it as a valid type for the ABI boundary. + property int TrigUnitMode + { + void set(int value) + { + if (value != (int)m_solver->EvalOptions().GetTrigUnitMode()) + { + m_solver->EvalOptions().SetTrigUnitMode((Graphing::EvalTrigUnitMode)value); + PlotGraph(true); + } + } + + int get() + { + return (int)m_solver->EvalOptions().GetTrigUnitMode(); + } + } + + property double XAxisMin + { + double get() + { + return m_graph->GetOptions().GetDefaultXRange().first; + } + void set(double value) + { + std::pair newValue(value, XAxisMax); + if (m_graph != nullptr) + { + m_graph->GetOptions().SetDefaultXRange(newValue); + if (m_renderMain != nullptr) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + property double XAxisMax + { + double get() + { + return m_graph->GetOptions().GetDefaultXRange().second; + } + void set(double value) + { + std::pair newValue(XAxisMin, value); + if (m_graph != nullptr) + { + m_graph->GetOptions().SetDefaultXRange(newValue); + if (m_renderMain != nullptr) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + property double YAxisMin + { + double get() + { + return m_graph->GetOptions().GetDefaultXRange().first; + } + void set(double value) + { + std::pair newValue(value, YAxisMax); + if (m_graph != nullptr) + { + m_graph->GetOptions().SetDefaultYRange(newValue); + if (m_renderMain != nullptr) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + property double YAxisMax + { + double get() + { + return m_graph->GetOptions().GetDefaultXRange().second; + } + void set(double value) + { + std::pair newValue(YAxisMin, value); + if (m_graph != nullptr) + { + m_graph->GetOptions().SetDefaultYRange(newValue); + if (m_renderMain != nullptr) + { + m_renderMain->RunRenderPass(); + } + } + } + } + + void GetDisplayRanges(double* xMin, double* xMax, double* yMin, double* yMax) + { + try + { + if (m_graph != nullptr) + { + if (auto render = m_graph->GetRenderer()) + { + render->GetDisplayRanges(*xMin, *xMax, *yMin, *yMax); + } + } + } + catch (const std::exception&) + { + OutputDebugString(L"GetDisplayRanges failed\r\n"); + } + } + + void SetDisplayRanges(double xMin, double xMax, double yMin, double yMax) + { + try + { + if (auto render = m_graph->GetRenderer()) + { + render->SetDisplayRanges(xMin, xMax, yMin, yMax); + if (m_renderMain) + { + m_renderMain->RunRenderPass(); + } + } + } + catch (const std::exception&) + { + OutputDebugString(L"SetDisplayRanges failed\r\n"); + } + } + + protected: +#pragma region Control Overrides + void OnApplyTemplate() override; + + void OnPointerEntered(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnPointerMoved(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnPointerExited(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnPointerWheelChanged(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnPointerPressed(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnPointerReleased(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnPointerCanceled(Windows::UI::Xaml::Input::PointerRoutedEventArgs ^ e) override; + void OnManipulationDelta(Windows::UI::Xaml::Input::ManipulationDeltaRoutedEventArgs ^ e) override; +#pragma endregion + + private: + void OnForceProportionalAxesPropertyChanged(bool oldValue, bool newValue); + void OnEquationsPropertyChanged(EquationCollection ^ oldValue, EquationCollection ^ newValue); + void OnAxesColorPropertyChanged(Windows::UI::Color oldValue, Windows::UI::Color newValue); + void OnGraphBackgroundPropertyChanged(Windows::UI::Color oldValue, Windows::UI::Color newValue); + void OnEquationChanged(Equation ^ equation); + void OnEquationStyleChanged(Equation ^ equation); + void OnEquationLineEnabledChanged(Equation ^ equation); + bool TryUpdateGraph(bool keepCurrentView); + void TryPlotGraph(bool keepCurrentView, bool shouldRetry); + void UpdateGraphOptions(Graphing::IGraphingOptions& options, const std::vector& validEqs); + std::vector GetGraphableEquations(); + void SetGraphArgs(); + std::shared_ptr GetGraph(GraphControl::Equation ^ equation); + void UpdateVariables(); + + void ScaleRange(double centerX, double centerY, double scale); + + void OnCoreKeyDown(Windows::UI::Core::CoreWindow ^ sender, Windows::UI::Core::KeyEventArgs ^ e); + void OnCoreKeyUp(Windows::UI::Core::CoreWindow ^ sender, Windows::UI::Core::KeyEventArgs ^ e); + + void UpdateTracingChanged(); + void HandleTracingMovementTick(Object ^ sender, Object ^ e); + void HandleKey(bool keyDown, Windows::System::VirtualKey key); + + void SetEquationsAsValid(); + void SetEquationErrors(); + std::optional>> TryInitializeGraph(bool keepCurrentView, _In_ const Graphing::IExpression* graphingExp = nullptr); + private: + DX::RenderMain ^ m_renderMain = nullptr; + + static Windows::UI::Xaml::DependencyProperty ^ s_equationTemplateProperty; + + static Windows::UI::Xaml::DependencyProperty ^ s_equationsSourceProperty; + Windows::Foundation::EventRegistrationToken m_tokenDataSourceChanged; + + static Windows::UI::Xaml::DependencyProperty ^ s_equationsProperty; + static Windows::UI::Xaml::DependencyProperty ^ s_variablesProperty; + Windows::Foundation::EventRegistrationToken m_tokenEquationsChanged; + Windows::Foundation::EventRegistrationToken m_tokenEquationStyleChanged; + Windows::Foundation::EventRegistrationToken m_tokenEquationChanged; + Windows::Foundation::EventRegistrationToken m_tokenEquationLineEnabledChanged; + + Windows::Foundation::EventRegistrationToken m_tokenBackgroundColorChanged; + + const std::unique_ptr m_solver; + const std::shared_ptr m_graph; + bool m_calculatedForceProportional = false; + bool m_tracingTracking; + enum KeysPressedSlots + { + Left, + Right, + Down, + Up, + Accelerator + }; + + bool m_KeysPressed[5]; + bool m_Moving; + Windows::UI::Xaml::DispatcherTimer ^ m_TracingTrackingTimer; + + public: + Windows::Storage::Streams::RandomAccessStreamReference ^ GetGraphBitmapStream(); + }; +} diff --git a/src/GraphControl/DirectX/DeviceResources.cpp b/src/GraphControl/DirectX/DeviceResources.cpp new file mode 100644 index 000000000..29e0415b8 --- /dev/null +++ b/src/GraphControl/DirectX/DeviceResources.cpp @@ -0,0 +1,662 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "DeviceResources.h" +#include "DirectXHelper.h" + +using namespace D2D1; +using namespace DirectX; +using namespace Microsoft::WRL; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Graphics::Display; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml::Controls; +using namespace Platform; + +namespace DisplayMetrics +{ + // High resolution displays can require a lot of GPU and battery power to render. + // High resolution phones, for example, may suffer from poor battery life if + // games attempt to render at 60 frames per second at full fidelity. + // The decision to render at full fidelity across all platforms and form factors + // should be deliberate. + static constexpr bool SupportHighResolutions = false; + + // The default thresholds that define a "high resolution" display. If the thresholds + // are exceeded and SupportHighResolutions is false, the dimensions will be scaled + // by 50%. + static constexpr float DpiThreshold = 192.0f; // 200% of standard desktop display. + static constexpr float WidthThreshold = 1920.0f; // 1080p width. + static constexpr float HeightThreshold = 1080.0f; // 1080p height. +}; + +// Constants used to calculate screen rotations. +namespace ScreenRotation +{ + // 0-degree Z-rotation + static constexpr XMFLOAT4X4 Rotation0( + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); + + // 90-degree Z-rotation + static constexpr XMFLOAT4X4 Rotation90( + 0.0f, 1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); + + // 180-degree Z-rotation + static constexpr XMFLOAT4X4 Rotation180( + -1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, -1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); + + // 270-degree Z-rotation + static constexpr XMFLOAT4X4 Rotation270( + 0.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); +}; + +namespace GraphControl::DX +{ + // Constructor for DeviceResources. + DeviceResources::DeviceResources(SwapChainPanel^ panel) : + m_screenViewport(), + m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1), + m_d3dRenderTargetSize(), + m_outputSize(), + m_logicalSize(), + m_nativeOrientation(DisplayOrientations::None), + m_currentOrientation(DisplayOrientations::None), + m_dpi(-1.0f), + m_effectiveDpi(-1.0f), + m_compositionScaleX(1.0f), + m_compositionScaleY(1.0f), + m_deviceNotify(nullptr) + { + CreateDeviceIndependentResources(); + CreateDeviceResources(); + SetSwapChainPanel(panel); + } + + // Configures resources that don't depend on the Direct3D device. + void DeviceResources::CreateDeviceIndependentResources() + { + // Initialize Direct2D resources. + D2D1_FACTORY_OPTIONS options; + ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS)); + +#if defined(_DEBUG) + // If the project is in a debug build, enable Direct2D debugging via SDK Layers. + options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; +#endif + + // Initialize the Direct2D Factory. + DX::ThrowIfFailed( + D2D1CreateFactory( + D2D1_FACTORY_TYPE_SINGLE_THREADED, + __uuidof(ID2D1Factory3), + &options, + &m_d2dFactory + ) + ); + + // Initialize the DirectWrite Factory. + DX::ThrowIfFailed( + DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory3), + &m_dwriteFactory + ) + ); + + // Initialize the Windows Imaging Component (WIC) Factory. + DX::ThrowIfFailed( + CoCreateInstance( + CLSID_WICImagingFactory2, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&m_wicFactory) + ) + ); + } + + // Configures the Direct3D device, and stores handles to it and the device context. + void DeviceResources::CreateDeviceResources() + { + // This flag adds support for surfaces with a different color channel ordering + // than the API default. It is required for compatibility with Direct2D. + UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + +#if defined(_DEBUG) + if (DX::SdkLayersAvailable()) + { + // If the project is in a debug build, enable debugging via SDK Layers with this flag. + creationFlags |= D3D11_CREATE_DEVICE_DEBUG; + } +#endif + + // This array defines the set of DirectX hardware feature levels this app will support. + // Note the ordering should be preserved. + // Don't forget to declare your application's minimum required feature level in its + // description. All applications are assumed to support 9.1 unless otherwise stated. + static constexpr UINT featureLevelsSize = 9; + static constexpr std::array featureLevels = + { + D3D_FEATURE_LEVEL_12_1, + D3D_FEATURE_LEVEL_12_0, + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; + + // Create the Direct3D 11 API device object and a corresponding context. + ComPtr device; + ComPtr context; + + HRESULT hr = D3D11CreateDevice( + nullptr, // Specify nullptr to use the default adapter. + D3D_DRIVER_TYPE_HARDWARE, // Create a device using the hardware graphics driver. + 0, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE. + creationFlags, // Set debug and Direct2D compatibility flags. + &featureLevels[0], // List of feature levels this app can support. + featureLevelsSize, // Size of the list above. + D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Windows Store apps. + &device, // Returns the Direct3D device created. + &m_d3dFeatureLevel, // Returns feature level of device created. + &context // Returns the device immediate context. + ); + + if (FAILED(hr)) + { + // If the initialization fails, fall back to the WARP device. + // For more information on WARP, see: + // https://go.microsoft.com/fwlink/?LinkId=286690 + DX::ThrowIfFailed( + D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device. + 0, + creationFlags, + &featureLevels[0], + featureLevelsSize, + D3D11_SDK_VERSION, + &device, + &m_d3dFeatureLevel, + &context + ) + ); + } + + // Store pointers to the Direct3D 11.3 API device and immediate context. + DX::ThrowIfFailed( + device.As(&m_d3dDevice) + ); + + DX::ThrowIfFailed( + context.As(&m_d3dContext) + ); + + // Create the Direct2D device object and a corresponding context. + ComPtr dxgiDevice; + DX::ThrowIfFailed( + m_d3dDevice.As(&dxgiDevice) + ); + + DX::ThrowIfFailed( + m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice) + ); + + DX::ThrowIfFailed( + m_d2dDevice->CreateDeviceContext( + D2D1_DEVICE_CONTEXT_OPTIONS_NONE, + &m_d2dContext + ) + ); + } + + // These resources need to be recreated every time the window size is changed. + void DeviceResources::CreateWindowSizeDependentResources() + { + // Clear the previous window size specific context. + static constexpr std::array nullViews = { nullptr }; + m_d3dContext->OMSetRenderTargets(static_cast(nullViews.size()), &nullViews[0], nullptr); + m_d3dRenderTargetView = nullptr; + m_d2dContext->SetTarget(nullptr); + m_d2dTargetBitmap = nullptr; + m_d3dDepthStencilView = nullptr; + m_d3dContext->Flush1(D3D11_CONTEXT_TYPE_ALL, nullptr); + + UpdateRenderTargetSize(); + + m_d3dRenderTargetSize.Width = m_outputSize.Width; + m_d3dRenderTargetSize.Height = m_outputSize.Height; + + if (m_swapChain != nullptr) + { + // If the swap chain already exists, resize it. + HRESULT hr = m_swapChain->ResizeBuffers( + 2, // Double-buffered swap chain. + lround(m_d3dRenderTargetSize.Width), + lround(m_d3dRenderTargetSize.Height), + DXGI_FORMAT_B8G8R8A8_UNORM, + 0 + ); + + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + { + // If the device was removed for any reason, a new device and swap chain will need to be created. + HandleDeviceLost(); + + // Everything is set up now. Do not continue execution of this method. HandleDeviceLost will reenter this method + // and correctly set up the new device. + return; + } + else + { + DX::ThrowIfFailed(hr); + } + } + else + { + // Otherwise, create a new one using the same adapter as the existing Direct3D device. + DXGI_SCALING scaling = DisplayMetrics::SupportHighResolutions ? DXGI_SCALING_NONE : DXGI_SCALING_STRETCH; + DXGI_SWAP_CHAIN_DESC1 swapChainDesc = { 0 }; + + swapChainDesc.Width = lround(m_d3dRenderTargetSize.Width); // Match the size of the window. + swapChainDesc.Height = lround(m_d3dRenderTargetSize.Height); + swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // This is the most common swap chain format. + swapChainDesc.Stereo = false; + swapChainDesc.SampleDesc.Count = 1; // Don't use multi-sampling. + swapChainDesc.SampleDesc.Quality = 0; + swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + swapChainDesc.BufferCount = 2; // Use double-buffering to minimize latency. + swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // All Windows Store apps must use _FLIP_ SwapEffects. + swapChainDesc.Flags = 0; + swapChainDesc.Scaling = scaling; + swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + + // This sequence obtains the DXGI factory that was used to create the Direct3D device above. + ComPtr dxgiDevice; + DX::ThrowIfFailed( + m_d3dDevice.As(&dxgiDevice) + ); + + ComPtr dxgiAdapter; + DX::ThrowIfFailed( + dxgiDevice->GetAdapter(&dxgiAdapter) + ); + + ComPtr dxgiFactory; + DX::ThrowIfFailed( + dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory)) + ); + + // When using XAML interop, the swap chain must be created for composition. + ComPtr swapChain; + DX::ThrowIfFailed( + dxgiFactory->CreateSwapChainForComposition( + m_d3dDevice.Get(), + &swapChainDesc, + nullptr, + &swapChain + ) + ); + + DX::ThrowIfFailed( + swapChain.As(&m_swapChain) + ); + + // Associate swap chain with SwapChainPanel + // UI changes will need to be dispatched back to the UI thread + m_swapChainPanel->Dispatcher->RunAsync(CoreDispatcherPriority::High, ref new DispatchedHandler([=]() + { + // Get backing native interface for SwapChainPanel + ComPtr panelNative; + DX::ThrowIfFailed( + reinterpret_cast(m_swapChainPanel)->QueryInterface(IID_PPV_ARGS(&panelNative)) + ); + + DX::ThrowIfFailed( + panelNative->SetSwapChain(m_swapChain.Get()) + ); + }, CallbackContext::Any)); + + // Ensure that DXGI does not queue more than one frame at a time. This both reduces latency and + // ensures that the application will only render after each VSync, minimizing power consumption. + DX::ThrowIfFailed( + dxgiDevice->SetMaximumFrameLatency(1) + ); + } + + // Set the proper orientation for the swap chain, and generate 2D and + // 3D matrix transformations for rendering to the rotated swap chain. + // Note the rotation angle for the 2D and 3D transforms are different. + // This is due to the difference in coordinate spaces. Additionally, + // the 3D matrix is specified explicitly to avoid rounding errors. + + m_orientationTransform2D = Matrix3x2F::Identity(); + m_orientationTransform3D = ScreenRotation::Rotation0; + + // Setup inverse scale on the swap chain + DXGI_MATRIX_3X2_F inverseScale = { 0 }; + inverseScale._11 = 1.0f / m_effectiveCompositionScaleX; + inverseScale._22 = 1.0f / m_effectiveCompositionScaleY; + ComPtr spSwapChain2; + DX::ThrowIfFailed( + m_swapChain.As(&spSwapChain2) + ); + + DX::ThrowIfFailed( + spSwapChain2->SetMatrixTransform(&inverseScale) + ); + + // Create a render target view of the swap chain back buffer. + ComPtr backBuffer; + DX::ThrowIfFailed( + m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)) + ); + + DX::ThrowIfFailed( + m_d3dDevice->CreateRenderTargetView1( + backBuffer.Get(), + nullptr, + &m_d3dRenderTargetView + ) + ); + + // Create a depth stencil view for use with 3D rendering if needed. + CD3D11_TEXTURE2D_DESC1 depthStencilDesc( + DXGI_FORMAT_D24_UNORM_S8_UINT, + lround(m_d3dRenderTargetSize.Width), + lround(m_d3dRenderTargetSize.Height), + 1, // This depth stencil view has only one texture. + 1, // Use a single mipmap level. + D3D11_BIND_DEPTH_STENCIL + ); + + ComPtr depthStencil; + DX::ThrowIfFailed( + m_d3dDevice->CreateTexture2D1( + &depthStencilDesc, + nullptr, + &depthStencil + ) + ); + + CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D); + DX::ThrowIfFailed( + m_d3dDevice->CreateDepthStencilView( + depthStencil.Get(), + &depthStencilViewDesc, + &m_d3dDepthStencilView + ) + ); + + // Set the 3D rendering viewport to target the entire window. + m_screenViewport = CD3D11_VIEWPORT( + 0.0f, + 0.0f, + m_d3dRenderTargetSize.Width, + m_d3dRenderTargetSize.Height + ); + + m_d3dContext->RSSetViewports(1, &m_screenViewport); + + // Create a Direct2D target bitmap associated with the + // swap chain back buffer and set it as the current target. + D2D1_BITMAP_PROPERTIES1 bitmapProperties = + D2D1::BitmapProperties1( + D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + m_dpi, + m_dpi + ); + + ComPtr dxgiBackBuffer; + DX::ThrowIfFailed( + m_swapChain->GetBuffer(0, IID_PPV_ARGS(&dxgiBackBuffer)) + ); + + DX::ThrowIfFailed( + m_d2dContext->CreateBitmapFromDxgiSurface( + dxgiBackBuffer.Get(), + &bitmapProperties, + &m_d2dTargetBitmap + ) + ); + + m_d2dContext->SetTarget(m_d2dTargetBitmap.Get()); + m_d2dContext->SetDpi(m_effectiveDpi, m_effectiveDpi); + + // Grayscale text anti-aliasing is recommended for all Windows Store apps. + m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + } + + // Determine the dimensions of the render target and whether it will be scaled down. + void DeviceResources::UpdateRenderTargetSize() + { + m_effectiveDpi = m_dpi; + m_effectiveCompositionScaleX = m_compositionScaleX; + m_effectiveCompositionScaleY = m_compositionScaleY; + + // To improve battery life on high resolution devices, render to a smaller render target + // and allow the GPU to scale the output when it is presented. + if (!DisplayMetrics::SupportHighResolutions && m_dpi > DisplayMetrics::DpiThreshold) + { + float width = DX::ConvertDipsToPixels(m_logicalSize.Width, m_dpi); + float height = DX::ConvertDipsToPixels(m_logicalSize.Height, m_dpi); + + // When the device is in portrait orientation, height > width. Compare the + // larger dimension against the width threshold and the smaller dimension + // against the height threshold. + if (max(width, height) > DisplayMetrics::WidthThreshold && min(width, height) > DisplayMetrics::HeightThreshold) + { + // To scale the app we change the effective DPI. Logical size does not change. + m_effectiveDpi /= 2.0f; + m_effectiveCompositionScaleX /= 2.0f; + m_effectiveCompositionScaleY /= 2.0f; + } + } + + // Calculate the necessary render target size in pixels. + m_outputSize.Width = DX::ConvertDipsToPixels(m_logicalSize.Width, m_effectiveDpi); + m_outputSize.Height = DX::ConvertDipsToPixels(m_logicalSize.Height, m_effectiveDpi); + + // Prevent zero size DirectX content from being created. + m_outputSize.Width = max(m_outputSize.Width, 1.0f); + m_outputSize.Height = max(m_outputSize.Height, 1.0f); + } + + // This method is called when the XAML control is created (or re-created). + void DeviceResources::SetSwapChainPanel(SwapChainPanel^ panel) + { + DisplayInformation^ currentDisplayInformation = DisplayInformation::GetForCurrentView(); + + m_swapChainPanel = panel; + m_logicalSize = Windows::Foundation::Size(static_cast(panel->ActualWidth), static_cast(panel->ActualHeight)); + m_nativeOrientation = currentDisplayInformation->NativeOrientation; + m_currentOrientation = currentDisplayInformation->CurrentOrientation; + m_compositionScaleX = panel->CompositionScaleX; + m_compositionScaleY = panel->CompositionScaleY; + m_dpi = currentDisplayInformation->LogicalDpi; + m_d2dContext->SetDpi(m_dpi, m_dpi); + + CreateWindowSizeDependentResources(); + } + + // This method is called in the event handler for the SizeChanged event. + void DeviceResources::SetLogicalSize(Windows::Foundation::Size logicalSize) + { + if (m_logicalSize != logicalSize) + { + m_logicalSize = logicalSize; + CreateWindowSizeDependentResources(); + } + } + + // This method is called in the event handler for the DpiChanged event. + void DeviceResources::SetDpi(float dpi) + { + if (dpi != m_dpi) + { + m_dpi = dpi; + m_d2dContext->SetDpi(m_dpi, m_dpi); + CreateWindowSizeDependentResources(); + } + } + + // This method is called in the event handler for the OrientationChanged event. + void DeviceResources::SetCurrentOrientation(DisplayOrientations currentOrientation) + { + if (m_currentOrientation != currentOrientation) + { + m_currentOrientation = currentOrientation; + CreateWindowSizeDependentResources(); + } + } + + // This method is called in the event handler for the CompositionScaleChanged event. + void DeviceResources::SetCompositionScale(float compositionScaleX, float compositionScaleY) + { + if (m_compositionScaleX != compositionScaleX || + m_compositionScaleY != compositionScaleY) + { + m_compositionScaleX = compositionScaleX; + m_compositionScaleY = compositionScaleY; + CreateWindowSizeDependentResources(); + } + } + + // This method is called in the event handler for the DisplayContentsInvalidated event. + void DeviceResources::ValidateDevice() + { + // The D3D Device is no longer valid if the default adapter changed since the device + // was created or if the device has been removed. + + // First, get the information for the default adapter from when the device was created. + + ComPtr dxgiDevice; + DX::ThrowIfFailed(m_d3dDevice.As(&dxgiDevice)); + + ComPtr deviceAdapter; + DX::ThrowIfFailed(dxgiDevice->GetAdapter(&deviceAdapter)); + + ComPtr deviceFactory; + DX::ThrowIfFailed(deviceAdapter->GetParent(IID_PPV_ARGS(&deviceFactory))); + + ComPtr previousDefaultAdapter; + DX::ThrowIfFailed(deviceFactory->EnumAdapters1(0, &previousDefaultAdapter)); + + DXGI_ADAPTER_DESC1 previousDesc; + DX::ThrowIfFailed(previousDefaultAdapter->GetDesc1(&previousDesc)); + + // Next, get the information for the current default adapter. + + ComPtr currentFactory; + DX::ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(¤tFactory))); + + ComPtr currentDefaultAdapter; + DX::ThrowIfFailed(currentFactory->EnumAdapters1(0, ¤tDefaultAdapter)); + + DXGI_ADAPTER_DESC1 currentDesc; + DX::ThrowIfFailed(currentDefaultAdapter->GetDesc1(¤tDesc)); + + // If the adapter LUIDs don't match, or if the device reports that it has been removed, + // a new D3D device must be created. + + if (previousDesc.AdapterLuid.LowPart != currentDesc.AdapterLuid.LowPart || + previousDesc.AdapterLuid.HighPart != currentDesc.AdapterLuid.HighPart || + FAILED(m_d3dDevice->GetDeviceRemovedReason())) + { + // Release references to resources related to the old device. + dxgiDevice = nullptr; + deviceAdapter = nullptr; + deviceFactory = nullptr; + previousDefaultAdapter = nullptr; + + // Create a new device and swap chain. + HandleDeviceLost(); + } + } + + // Recreate all device resources and set them back to the current state. + void DeviceResources::HandleDeviceLost() + { + m_swapChain = nullptr; + + if (m_deviceNotify != nullptr) + { + m_deviceNotify->OnDeviceLost(); + } + + CreateDeviceResources(); + m_d2dContext->SetDpi(m_dpi, m_dpi); + CreateWindowSizeDependentResources(); + + if (m_deviceNotify != nullptr) + { + m_deviceNotify->OnDeviceRestored(); + } + } + + // Register our DeviceNotify to be informed on device lost and creation. + void DeviceResources::RegisterDeviceNotify(DX::IDeviceNotify^ deviceNotify) + { + m_deviceNotify = deviceNotify; + } + + // Call this method when the app suspends. It provides a hint to the driver that the app + // is entering an idle state and that temporary buffers can be reclaimed for use by other apps. + void DeviceResources::Trim() + { + ComPtr dxgiDevice; + m_d3dDevice.As(&dxgiDevice); + + dxgiDevice->Trim(); + } + + // Present the contents of the swap chain to the screen. + void DeviceResources::Present() + { + // The first argument instructs DXGI to block until VSync, putting the application + // to sleep until the next VSync. This ensures we don't waste any cycles rendering + // frames that will never be displayed to the screen. + DXGI_PRESENT_PARAMETERS parameters = { 0 }; + HRESULT hr = m_swapChain->Present1(1, 0, ¶meters); + + // Discard the contents of the render target. + // This is a valid operation only when the existing contents will be entirely + // overwritten. If dirty or scroll rects are used, this call should be modified. + m_d3dContext->DiscardView1(m_d3dRenderTargetView.Get(), nullptr, 0); + + // Discard the contents of the depth stencil. + m_d3dContext->DiscardView1(m_d3dDepthStencilView.Get(), nullptr, 0); + + // If the device was removed either by a disconnection or a driver upgrade, we + // must recreate all device resources. + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + { + HandleDeviceLost(); + } + else + { + DX::ThrowIfFailed(hr); + } + } +} diff --git a/src/GraphControl/DirectX/DeviceResources.h b/src/GraphControl/DirectX/DeviceResources.h new file mode 100644 index 000000000..92072ec86 --- /dev/null +++ b/src/GraphControl/DirectX/DeviceResources.h @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Modified from the default template for Xaml and Direct3D 11 apps. + +namespace GraphControl::DX +{ + // Provides an interface for an application that owns DeviceResources to be notified of the device being lost or created. + interface class IDeviceNotify + { + virtual void OnDeviceLost(); + virtual void OnDeviceRestored(); + }; + + // Controls all the DirectX device resources. + class DeviceResources + { + public: + DeviceResources(Windows::UI::Xaml::Controls::SwapChainPanel^ panel); + void SetSwapChainPanel(Windows::UI::Xaml::Controls::SwapChainPanel^ panel); + void SetLogicalSize(Windows::Foundation::Size logicalSize); + void SetCurrentOrientation(Windows::Graphics::Display::DisplayOrientations currentOrientation); + void SetDpi(float dpi); + void SetCompositionScale(float compositionScaleX, float compositionScaleY); + void ValidateDevice(); + void HandleDeviceLost(); + void RegisterDeviceNotify(IDeviceNotify^ deviceNotify); + void Trim(); + void Present(); + + // The size of the render target, in pixels. + Windows::Foundation::Size GetOutputSize() const { return m_outputSize; } + + // The size of the render target, in dips. + Windows::Foundation::Size GetLogicalSize() const { return m_logicalSize; } + float GetDpi() const { return m_effectiveDpi; } + + // D3D Accessors. + ID3D11Device3* GetD3DDevice() const { return m_d3dDevice.Get(); } + ID3D11DeviceContext3* GetD3DDeviceContext() const { return m_d3dContext.Get(); } + IDXGISwapChain3* GetSwapChain() const { return m_swapChain.Get(); } + D3D_FEATURE_LEVEL GetDeviceFeatureLevel() const { return m_d3dFeatureLevel; } + ID3D11RenderTargetView1* GetBackBufferRenderTargetView() const { return m_d3dRenderTargetView.Get(); } + ID3D11DepthStencilView* GetDepthStencilView() const { return m_d3dDepthStencilView.Get(); } + D3D11_VIEWPORT GetScreenViewport() const { return m_screenViewport; } + DirectX::XMFLOAT4X4 GetOrientationTransform3D() const { return m_orientationTransform3D; } + + // D2D Accessors. + ID2D1Factory3* GetD2DFactory() const { return m_d2dFactory.Get(); } + ID2D1Device2* GetD2DDevice() const { return m_d2dDevice.Get(); } + ID2D1DeviceContext2* GetD2DDeviceContext() const { return m_d2dContext.Get(); } + ID2D1Bitmap1* GetD2DTargetBitmap() const { return m_d2dTargetBitmap.Get(); } + IDWriteFactory3* GetDWriteFactory() const { return m_dwriteFactory.Get(); } + IWICImagingFactory2* GetWicImagingFactory() const { return m_wicFactory.Get(); } + D2D1::Matrix3x2F GetOrientationTransform2D() const { return m_orientationTransform2D; } + + private: + void CreateDeviceIndependentResources(); + void CreateDeviceResources(); + void CreateWindowSizeDependentResources(); + void UpdateRenderTargetSize(); + + // Direct3D objects. + Microsoft::WRL::ComPtr m_d3dDevice; + Microsoft::WRL::ComPtr m_d3dContext; + Microsoft::WRL::ComPtr m_swapChain; + + // Direct3D rendering objects. Required for 3D. + Microsoft::WRL::ComPtr m_d3dRenderTargetView; + Microsoft::WRL::ComPtr m_d3dDepthStencilView; + D3D11_VIEWPORT m_screenViewport; + + // Direct2D drawing components. + Microsoft::WRL::ComPtr m_d2dFactory; + Microsoft::WRL::ComPtr m_d2dDevice; + Microsoft::WRL::ComPtr m_d2dContext; + Microsoft::WRL::ComPtr m_d2dTargetBitmap; + + // DirectWrite drawing components. + Microsoft::WRL::ComPtr m_dwriteFactory; + Microsoft::WRL::ComPtr m_wicFactory; + + // Cached reference to the XAML panel. + Windows::UI::Xaml::Controls::SwapChainPanel^ m_swapChainPanel; + + // Cached device properties. + D3D_FEATURE_LEVEL m_d3dFeatureLevel; + Windows::Foundation::Size m_d3dRenderTargetSize; + Windows::Foundation::Size m_outputSize; + Windows::Foundation::Size m_logicalSize; + Windows::Graphics::Display::DisplayOrientations m_nativeOrientation; + Windows::Graphics::Display::DisplayOrientations m_currentOrientation; + float m_dpi; + float m_compositionScaleX; + float m_compositionScaleY; + + // Variables that take into account whether the app supports high resolution screens or not. + float m_effectiveDpi; + float m_effectiveCompositionScaleX; + float m_effectiveCompositionScaleY; + + // Transforms used for display orientation. + D2D1::Matrix3x2F m_orientationTransform2D; + DirectX::XMFLOAT4X4 m_orientationTransform3D; + + // The IDeviceNotify can be held directly as it owns the DeviceResources. + IDeviceNotify^ m_deviceNotify; + }; +} diff --git a/src/GraphControl/DirectX/DirectXHelper.h b/src/GraphControl/DirectX/DirectXHelper.h new file mode 100644 index 000000000..9e74ce773 --- /dev/null +++ b/src/GraphControl/DirectX/DirectXHelper.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Taken from the default template for Xaml and Direct3D 11 apps. + +namespace GraphControl::DX +{ + inline void ThrowIfFailed(HRESULT hr) + { + if (FAILED(hr)) + { + // Set a breakpoint on this line to catch Win32 API errors. + throw Platform::Exception::CreateException(hr); + } + } + + // Function that reads from a binary file asynchronously. + inline Concurrency::task> ReadDataAsync(const std::wstring& filename) + { + using namespace Windows::Storage; + using namespace Concurrency; + + auto folder = Windows::ApplicationModel::Package::Current->InstalledLocation; + + return create_task(folder->GetFileAsync(Platform::StringReference(filename.c_str()))).then([] (StorageFile^ file) + { + return FileIO::ReadBufferAsync(file); + }).then([] (Streams::IBuffer^ fileBuffer) -> std::vector + { + std::vector returnBuffer; + returnBuffer.resize(fileBuffer->Length); + Streams::DataReader::FromBuffer(fileBuffer)->ReadBytes(Platform::ArrayReference(returnBuffer.data(), fileBuffer->Length)); + return returnBuffer; + }); + } + + // Converts a length in device-independent pixels (DIPs) to a length in physical pixels. + inline float ConvertDipsToPixels(float dips, float dpi) + { + static const float dipsPerInch = 96.0f; + return floorf(dips * dpi / dipsPerInch + 0.5f); // Round to nearest integer. + } + +#if defined(_DEBUG) + // Check for SDK Layer support. + inline bool SdkLayersAvailable() + { + HRESULT hr = D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_NULL, // There is no need to create a real hardware device. + 0, + D3D11_CREATE_DEVICE_DEBUG, // Check for the SDK layers. + nullptr, // Any feature level will do. + 0, + D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Windows Store apps. + nullptr, // No need to keep the D3D device reference. + nullptr, // No need to know the feature level. + nullptr // No need to keep the D3D device context reference. + ); + + return SUCCEEDED(hr); + } +#endif +} diff --git a/src/GraphControl/DirectX/NearestPointRenderer.cpp b/src/GraphControl/DirectX/NearestPointRenderer.cpp new file mode 100644 index 000000000..64b114e33 --- /dev/null +++ b/src/GraphControl/DirectX/NearestPointRenderer.cpp @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NearestPointRenderer.h" +#include "DirectXHelper.h" + +using namespace D2D1; +using namespace GraphControl::DX; +using namespace std; +using namespace Windows::Foundation; + +namespace +{ + const ColorF c_DefaultPointColor = ColorF::Black; + constexpr float c_NearestPointRadius = 3; +} + +NearestPointRenderer::NearestPointRenderer(DeviceResources* deviceResources) + : m_deviceResources{ deviceResources } + , m_color{ c_DefaultPointColor } + , m_ellipse{ D2D1_POINT_2F{ 0, 0 }, c_NearestPointRadius, c_NearestPointRadius } +{ + CreateDeviceDependentResources(); +} + +void NearestPointRenderer::CreateDeviceDependentResources() +{ + CreateBrush(); +} + +void NearestPointRenderer::ReleaseDeviceDependentResources() +{ + m_brush.Reset(); +} + +void NearestPointRenderer::Render(const Point& location) +{ + if (ID2D1DeviceContext* context = m_deviceResources->GetD2DDeviceContext()) + { + m_ellipse.point.x = location.X; + m_ellipse.point.y = location.Y; + + context->BeginDraw(); + context->FillEllipse(m_ellipse, m_brush.Get()); + + // Ignore D2DERR_RECREATE_TARGET here. This error indicates that the device + // is lost. It will be handled during the next call to Present. + HRESULT hr = context->EndDraw(); + if (hr != D2DERR_RECREATE_TARGET) + { + ThrowIfFailed(hr); + } + } +} + +void NearestPointRenderer::SetColor(const ColorF& color) +{ + m_color = color; + CreateBrush(); +} + +void NearestPointRenderer::CreateBrush() +{ + m_brush.Reset(); + ThrowIfFailed( + m_deviceResources->GetD2DDeviceContext()->CreateSolidColorBrush(m_color, &m_brush) + ); +} diff --git a/src/GraphControl/DirectX/NearestPointRenderer.h b/src/GraphControl/DirectX/NearestPointRenderer.h new file mode 100644 index 000000000..791496545 --- /dev/null +++ b/src/GraphControl/DirectX/NearestPointRenderer.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace GraphControl::DX +{ + class DeviceResources; + + class NearestPointRenderer + { + public: + NearestPointRenderer(DeviceResources* deviceResources); + + void CreateDeviceDependentResources(); + void ReleaseDeviceDependentResources(); + void Render(const Windows::Foundation::Point& location); + + void SetColor(const D2D1::ColorF& color); + + private: + void CreateBrush(); + + private: + DeviceResources* const m_deviceResources; + + D2D1::ColorF m_color; + D2D1_ELLIPSE m_ellipse; + + // Resources related to rendering. + Microsoft::WRL::ComPtr m_brush; + }; +} diff --git a/src/GraphControl/DirectX/RenderMain.cpp b/src/GraphControl/DirectX/RenderMain.cpp new file mode 100644 index 000000000..4e54aaacc --- /dev/null +++ b/src/GraphControl/DirectX/RenderMain.cpp @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "RenderMain.h" +#include "DirectXHelper.h" + +using namespace Concurrency; +using namespace Graphing; +using namespace Platform; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Graphics::Display; +using namespace Windows::System::Threading; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace +{ + constexpr unsigned int s_RedChannelIndex = 0; + constexpr unsigned int s_GreenChannelIndex = 1; + constexpr unsigned int s_BlueChannelIndex = 2; + constexpr unsigned int s_AlphaChannelIndex = 3; + constexpr float s_MaxChannelValue = 255.0f; + + constexpr float nearestPointRadius = 3; +} + +namespace GraphControl::DX +{ + RenderMain::RenderMain(SwapChainPanel ^ panel) + : m_deviceResources{ panel } + , m_nearestPointRenderer{ &m_deviceResources } + , m_backgroundColor{ {} } + , m_swapChainPanel{ panel } + , m_TraceValue(Point(0, 0)) + , m_TraceLocation(Point(0, 0)) + , m_Tracing(false) + { + // Register to be notified if the Device is lost or recreated + m_deviceResources.RegisterDeviceNotify(this); + + RegisterEventHandlers(); + + m_drawActiveTracing = false; + m_activeTracingPointerLocation.X = 50; + m_activeTracingPointerLocation.Y = 50; + } + + RenderMain::~RenderMain() + { + UnregisterEventHandlers(); + } + + void RenderMain::Graph::set(shared_ptr graph) + { + m_graph = move(graph); + + if (m_graph) + { + if (auto renderer = m_graph->GetRenderer()) + { + float dpi = m_deviceResources.GetDpi(); + renderer->SetDpi(dpi, dpi); + + renderer->SetGraphSize(static_cast(m_swapChainPanel->ActualWidth), static_cast(m_swapChainPanel->ActualHeight)); + } + } + } + + void RenderMain::BackgroundColor::set(Windows::UI::Color backgroundColor) + { + m_backgroundColor[s_RedChannelIndex] = static_cast(backgroundColor.R) / s_MaxChannelValue; + m_backgroundColor[s_GreenChannelIndex] = static_cast(backgroundColor.G) / s_MaxChannelValue; + m_backgroundColor[s_BlueChannelIndex] = static_cast(backgroundColor.B) / s_MaxChannelValue; + m_backgroundColor[s_AlphaChannelIndex] = static_cast(backgroundColor.A) / s_MaxChannelValue; + + RunRenderPass(); + } + + void RenderMain::DrawNearestPoint::set(bool value) + { + if (m_drawNearestPoint != value) + { + m_drawNearestPoint = value; + if (!m_drawNearestPoint) + { + m_Tracing = false; + } + RunRenderPass(); + } + } + + void RenderMain::PointerLocation::set(Point location) + { + if (m_pointerLocation != location) + { + m_pointerLocation = location; + RunRenderPass(); + } + } + + void RenderMain::ActiveTracing::set(bool value) + { + if (m_drawActiveTracing != value) + { + m_drawActiveTracing = value; + RunRenderPass(); + } + } + + bool RenderMain::ActiveTracing::get() + { + return m_drawActiveTracing; + } + + // Updates application state when the window size changes (e.g. device orientation change) + void RenderMain::CreateWindowSizeDependentResources() + { + // TODO: Replace this with the sizedependent initialization of your app's content. + RunRenderPass(); + } + + bool RenderMain::RunRenderPass() + { + bool succesful = Render(); + + if (succesful) + { + m_deviceResources.Present(); + } + + return succesful; + } + + // Renders the current frame according to the current application state. + // Returns true if the frame was rendered and is ready to be displayed. + bool RenderMain::Render() + { + bool successful = true; + + // Must call BeginDraw before any draw commands. + ID2D1Factory3* pFactory = m_deviceResources.GetD2DFactory(); + ID2D1DeviceContext* pRenderTarget = m_deviceResources.GetD2DDeviceContext(); + + auto context = m_deviceResources.GetD3DDeviceContext(); + + // Clear the back buffer and set the background color. + context->ClearRenderTargetView(m_deviceResources.GetBackBufferRenderTargetView(), m_backgroundColor); + + if (m_graph) + { + if (auto renderer = m_graph->GetRenderer()) + { + pRenderTarget->BeginDraw(); + + bool hasMissingData = false; + successful = SUCCEEDED(renderer->DrawD2D1(pFactory, pRenderTarget, hasMissingData)); + + // We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device + // is lost. It will be handled during the next call to Present. + HRESULT endDraw = pRenderTarget->EndDraw(); + if (endDraw != D2DERR_RECREATE_TARGET) + { + DX::ThrowIfFailed(endDraw); + } + + if (successful) + { + if (m_drawNearestPoint || m_drawActiveTracing) + { + Point trackPoint = m_pointerLocation; + if (m_drawActiveTracing) + { + // Active tracing takes over for draw nearest point input from the mouse pointer. + trackPoint = m_activeTracingPointerLocation; + } + + int formulaId = -1; + float nearestPointLocationX, nearestPointLocationY; + float nearestPointValueX, nearestPointValueY; + + if (renderer->GetClosePointData( + trackPoint.X, trackPoint.Y, formulaId, nearestPointLocationX, nearestPointLocationY, nearestPointValueX, nearestPointValueY) + == S_OK) + { + if (!isnan(nearestPointLocationX) && !isnan(nearestPointLocationY)) + { + auto lineColors = m_graph->GetOptions().GetGraphColors(); + + if (formulaId >= 0 && static_cast(formulaId) < lineColors.size()) + { + auto dotColor = lineColors[formulaId]; + m_nearestPointRenderer.SetColor(D2D1::ColorF(dotColor.R * 65536 + dotColor.G * 256 + dotColor.B, 1.0)); + } + + m_TraceLocation = Point(nearestPointLocationX, nearestPointLocationY); + m_nearestPointRenderer.Render(m_TraceLocation); + m_Tracing = true; + m_TraceLocation = Point(nearestPointLocationX, nearestPointLocationY); + m_TraceValue = Point(nearestPointValueX, nearestPointValueY); + } + else + { + m_Tracing = false; + } + } + else + { + m_Tracing = false; + } + } + } + } + } + + return successful; + } + + void RenderMain::OnLoaded(Object ^ sender, RoutedEventArgs ^ e) + { + RunRenderPass(); + } + + void RenderMain::RegisterEventHandlers() + { + UnregisterEventHandlers(); + + // Register event handlers for control lifecycle. + m_coreWindow = Agile(Window::Current->CoreWindow); + if (m_coreWindow != nullptr) + { + m_tokenVisibilityChanged = m_coreWindow->VisibilityChanged += + ref new TypedEventHandler(this, &RenderMain::OnVisibilityChanged); + } + + m_displayInformation = DisplayInformation::GetForCurrentView(); + if (m_displayInformation != nullptr) + { + m_tokenDpiChanged = m_displayInformation->DpiChanged += ref new TypedEventHandler(this, &RenderMain::OnDpiChanged); + + m_tokenOrientationChanged = m_displayInformation->OrientationChanged += + ref new TypedEventHandler(this, &RenderMain::OnOrientationChanged); + } + + m_tokenDisplayContentsInvalidated = DisplayInformation::DisplayContentsInvalidated += + ref new TypedEventHandler(this, &RenderMain::OnDisplayContentsInvalidated); + + if (m_swapChainPanel != nullptr) + { + m_tokenLoaded = m_swapChainPanel->Loaded += ref new RoutedEventHandler(this, &RenderMain::OnLoaded); + + m_tokenCompositionScaleChanged = m_swapChainPanel->CompositionScaleChanged += + ref new TypedEventHandler(this, &RenderMain::OnCompositionScaleChanged); + + m_tokenSizeChanged = m_swapChainPanel->SizeChanged += ref new SizeChangedEventHandler(this, &RenderMain::OnSizeChanged); + } + } + + void RenderMain::UnregisterEventHandlers() + { + if (m_coreWindow != nullptr) + { + if (m_tokenVisibilityChanged.Value != 0) + { + m_coreWindow->VisibilityChanged -= m_tokenVisibilityChanged; + m_tokenVisibilityChanged.Value = 0; + } + m_coreWindow = nullptr; + } + + if (m_displayInformation != nullptr) + { + if (m_tokenDpiChanged.Value != 0) + { + m_displayInformation->DpiChanged -= m_tokenDpiChanged; + m_tokenDpiChanged.Value = 0; + } + if (m_tokenOrientationChanged.Value != 0) + { + m_displayInformation->OrientationChanged -= m_tokenOrientationChanged; + m_tokenOrientationChanged.Value = 0; + } + m_displayInformation = nullptr; + } + + if (m_tokenDisplayContentsInvalidated.Value != 0) + { + DisplayInformation::DisplayContentsInvalidated -= m_tokenDisplayContentsInvalidated; + m_tokenDisplayContentsInvalidated.Value = 0; + } + + if (m_swapChainPanel != nullptr) + { + if (m_tokenLoaded.Value != 0) + { + m_swapChainPanel->Loaded -= m_tokenLoaded; + m_tokenLoaded.Value = 0; + } + if (m_tokenCompositionScaleChanged.Value != 0) + { + m_swapChainPanel->CompositionScaleChanged -= m_tokenCompositionScaleChanged; + m_tokenCompositionScaleChanged.Value = 0; + } + if (m_tokenSizeChanged.Value != 0) + { + m_swapChainPanel->SizeChanged -= m_tokenSizeChanged; + m_tokenSizeChanged.Value = 0; + } + } + } + + void RenderMain::OnVisibilityChanged(CoreWindow ^ sender, VisibilityChangedEventArgs ^ args) + { + if (args->Visible) + { + RunRenderPass(); + } + } + + void RenderMain::OnDpiChanged(DisplayInformation ^ sender, Object ^ args) + { + // Note: The value for LogicalDpi retrieved here may not match the effective DPI of the app + // if it is being scaled for high resolution devices. Once the DPI is set on DeviceResources, + // you should always retrieve it using the GetDpi method. + // See DeviceResources.cpp for more details. + m_deviceResources.SetDpi(sender->LogicalDpi); + + if (m_graph) + { + if (auto renderer = m_graph->GetRenderer()) + { + float dpi = m_deviceResources.GetDpi(); + renderer->SetDpi(dpi, dpi); + } + } + + CreateWindowSizeDependentResources(); + } + + void RenderMain::OnOrientationChanged(DisplayInformation ^ sender, Object ^ args) + { + m_deviceResources.SetCurrentOrientation(sender->CurrentOrientation); + CreateWindowSizeDependentResources(); + } + + void RenderMain::OnDisplayContentsInvalidated(DisplayInformation ^ sender, Object ^ args) + { + m_deviceResources.ValidateDevice(); + } + + void RenderMain::OnCompositionScaleChanged(SwapChainPanel ^ sender, Object ^ args) + { + m_deviceResources.SetCompositionScale(sender->CompositionScaleX, sender->CompositionScaleY); + CreateWindowSizeDependentResources(); + } + + void RenderMain::OnSizeChanged(Object ^ sender, SizeChangedEventArgs ^ e) + { + m_deviceResources.SetLogicalSize(e->NewSize); + + if (m_graph) + { + if (auto renderer = m_graph->GetRenderer()) + { + const auto& newSize = e->NewSize; + renderer->SetGraphSize(static_cast(newSize.Width), static_cast(newSize.Height)); + } + } + + CreateWindowSizeDependentResources(); + } + + // Notifies renderers that device resources need to be released. + void RenderMain::OnDeviceLost() + { + m_nearestPointRenderer.ReleaseDeviceDependentResources(); + } + + // Notifies renderers that device resources may now be recreated. + void RenderMain::OnDeviceRestored() + { + m_nearestPointRenderer.CreateDeviceDependentResources(); + } +} diff --git a/src/GraphControl/DirectX/RenderMain.h b/src/GraphControl/DirectX/RenderMain.h new file mode 100644 index 000000000..7524e2ece --- /dev/null +++ b/src/GraphControl/DirectX/RenderMain.h @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Taken from the default template for Xaml and Direct3D 11 apps. + +#include "DeviceResources.h" +#include "NearestPointRenderer.h" +#include "IGraph.h" + +// Renders Direct2D and 3D content on the screen. +namespace GraphControl::DX +{ + ref class RenderMain sealed : public IDeviceNotify + { + public: + virtual ~RenderMain(); + + // IDeviceNotify + virtual void OnDeviceLost(); + virtual void OnDeviceRestored(); + + internal : RenderMain(Windows::UI::Xaml::Controls::SwapChainPanel ^ panel); + + property std::shared_ptr Graph + { + void set(std::shared_ptr graph); + } + + property Windows::UI::Color BackgroundColor + { + void set(Windows::UI::Color color); + } + + property bool DrawNearestPoint + { + void set(bool value); + } + + property Windows::Foundation::Point PointerLocation + { + void set(Windows::Foundation::Point location); + } + + void CreateWindowSizeDependentResources(); + + bool RunRenderPass(); + + // Indicates if we are in active tracing mode (the tracing box is being used and controlled through keyboard input) + property bool ActiveTracing + { + bool get(); + void set(bool value); + } + + property Windows::Foundation::Point ActiveTraceCursorPosition + { + Windows::Foundation::Point get() + { + return m_activeTracingPointerLocation; + } + + void set(Windows::Foundation::Point newValue) + { + if (m_activeTracingPointerLocation != newValue) + { + m_activeTracingPointerLocation = newValue; + RunRenderPass(); + } + } + } + + property Windows::Foundation::Point TraceValue + { + Windows::Foundation::Point get() + { + return m_TraceValue; + } + } + + property Windows::Foundation::Point TraceLocation + { + Windows::Foundation::Point get() + { + return m_TraceLocation; + } + } + + // Any time we should be showing the tracing popup (either active or passive tracing) + property bool Tracing + { + bool get() + { + return m_Tracing; + } + } + + private: + bool Render(); + + // Loaded/Unloaded + void OnLoaded(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); + + // Dependent event registration + void RegisterEventHandlers(); + void UnregisterEventHandlers(); + + // Window event handlers. + void OnVisibilityChanged(Windows::UI::Core::CoreWindow ^ sender, Windows::UI::Core::VisibilityChangedEventArgs ^ args); + + // DisplayInformation event handlers. + void OnDpiChanged(Windows::Graphics::Display::DisplayInformation ^ sender, Platform::Object ^ args); + void OnOrientationChanged(Windows::Graphics::Display::DisplayInformation ^ sender, Platform::Object ^ args); + void OnDisplayContentsInvalidated(Windows::Graphics::Display::DisplayInformation ^ sender, Platform::Object ^ args); + + // Other event handlers. + void OnCompositionScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel ^ sender, Object ^ args); + void OnSizeChanged(Platform::Object ^ sender, Windows::UI::Xaml::SizeChangedEventArgs ^ e); + + private: + DX::DeviceResources m_deviceResources; + NearestPointRenderer m_nearestPointRenderer; + + // Cached Graph object with Renderer property. + std::shared_ptr m_graph = nullptr; + + // Track current input pointer position. + bool m_drawNearestPoint = false; + Windows::Foundation::Point m_pointerLocation; + + // Track current active tracing pointer position. + bool m_drawActiveTracing = false; + Windows::Foundation::Point m_activeTracingPointerLocation; + + float m_backgroundColor[4]; + + // The SwapChainPanel^ surface. + Windows::UI::Xaml::Controls::SwapChainPanel ^ m_swapChainPanel = nullptr; + Windows::Foundation::EventRegistrationToken m_tokenLoaded; + Windows::Foundation::EventRegistrationToken m_tokenCompositionScaleChanged; + Windows::Foundation::EventRegistrationToken m_tokenSizeChanged; + + // Cached references to event notifiers. + Platform::Agile m_coreWindow = nullptr; + Windows::Foundation::EventRegistrationToken m_tokenVisibilityChanged; + + Windows::Graphics::Display::DisplayInformation ^ m_displayInformation = nullptr; + Windows::Foundation::EventRegistrationToken m_tokenDpiChanged; + Windows::Foundation::EventRegistrationToken m_tokenOrientationChanged; + Windows::Foundation::EventRegistrationToken m_tokenDisplayContentsInvalidated; + + // Track our independent input on a background worker thread. + Windows::Foundation::IAsyncAction ^ m_inputLoopWorker = nullptr; + Windows::UI::Core::CoreIndependentInputSource ^ m_coreInput = nullptr; + + // What is the current trace value + Windows::Foundation::Point m_TraceValue; + + // And where is it located on screen + Windows::Foundation::Point m_TraceLocation; + + // Are we currently showing the tracing value + bool m_Tracing; + }; +} diff --git a/src/GraphControl/GraphControl.vcxproj b/src/GraphControl/GraphControl.vcxproj new file mode 100644 index 000000000..d7c9f3b29 --- /dev/null +++ b/src/GraphControl/GraphControl.vcxproj @@ -0,0 +1,327 @@ + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {e727a92b-f149-492c-8117-c039a298719b} + WindowsRuntimeComponent + GraphControl + en-US + 14.0 + true + Windows Store + 10.0.18362.0 + 10.0.17134.0 + 10.0 + + + + DynamicLibrary + true + v142 + + + DynamicLibrary + true + v142 + + + DynamicLibrary + true + v142 + + + DynamicLibrary + true + v142 + + + DynamicLibrary + false + true + v142 + + + DynamicLibrary + false + true + v142 + + + DynamicLibrary + false + true + v142 + + + DynamicLibrary + false + true + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + true + + + $([MSBuild]::ValueOrDefault($(GraphingInterfaceDir), '$(SolutionDir)\GraphingInterfaces\')) + + + + + + + Use + _WINRT_DLL;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj /await %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + + + + + Use + _WINRT_DLL;NDEBUG;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj /await /d2CoroOptsWorkaround %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + Use + _WINRT_DLL;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj /await %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + Use + _WINRT_DLL;NDEBUG;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj /await /d2CoroOptsWorkaround %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + Use + _WINRT_DLL;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + Use + _WINRT_DLL;NDEBUG;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + Use + _WINRT_DLL;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj /await %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + Use + _WINRT_DLL;NDEBUG;%(PreprocessorDefinitions) + pch.h + $(IntDir)pch.pch + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + /bigobj /await /d2CoroOptsWorkaround %(AdditionalOptions) + 28204 + stdcpp17 + $(ProjectDir);$(GraphingInterfaceDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + + + Console + false + $(GraphingImplLib);WindowsApp.lib;%(AdditionalDependencies) + $(GraphingImplLibDir);%(AdditionalLibraryDirectories) + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + Create + Create + Create + Create + + + + + + + + {52E03A58-B378-4F50-8BFB-F659FB85E790} + + + + + \ No newline at end of file diff --git a/src/GraphControl/GraphControl.vcxproj.filters b/src/GraphControl/GraphControl.vcxproj.filters new file mode 100644 index 000000000..2b7c4e99e --- /dev/null +++ b/src/GraphControl/GraphControl.vcxproj.filters @@ -0,0 +1,75 @@ + + + + + {0d550f5f-db67-4160-8648-397c9bdc0307} + + + {3d424f3b-ba30-440b-ac2b-8a2740506153} + + + {e8d91a71-6933-4fd8-b333-421085d13896} + + + {0f768477-7ceb-42c4-a32a-cb024320dbc3} + + + + + + DirectX + + + DirectX + + + Control + + + DirectX + + + + Models + + + + + + DirectX + + + DirectX + + + DirectX + + + + Control + + + DirectX + + + + + Models + + + Models + + + + + Themes + + + + + + + + + + \ No newline at end of file diff --git a/src/GraphControl/Models/Equation.cpp b/src/GraphControl/Models/Equation.cpp new file mode 100644 index 000000000..3ca9313a2 --- /dev/null +++ b/src/GraphControl/Models/Equation.cpp @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Equation.h" + +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI; +using namespace Windows::UI::ViewManagement; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Media; + +namespace GraphControl +{ + // Remove mml: formatting specific to RichEditBox control, which is not understood by the graph engine. + static constexpr wstring_view s_mathPrefix = L"mml:"; + + Equation::Equation() + { + } + + String ^ Equation::GetRequest() + { + wstringstream ss; + wstring expr{ Expression->Data() }; + + // Check for unicode characters of less than, less than or equal to, greater than and greater than or equal to. + if (expr.find(L">><") != wstring::npos || expr.find(L"><<") != wstring::npos || expr.find(L">≥<") != wstring::npos + || expr.find(L">≤<") != wstring::npos) + { + ss << L"plotIneq2D"s; + } + else if (expr.find(L">=<") != wstring::npos) + { + ss << L"plotEq2d"; + } + else + { + ss << L"plot2d"; + } + ss << GetExpression() << L""; + + return ref new String(ss.str().c_str()); + } + + wstring Equation::GetExpression() + { + wstring mathML = Expression->Data(); + + size_t mathPrefix = 0; + while ((mathPrefix = mathML.find(s_mathPrefix, mathPrefix)) != std::string::npos) + { + mathML.replace(mathPrefix, s_mathPrefix.length(), L""); + mathPrefix += s_mathPrefix.length(); + } + + return mathML; + } + + Color Equation::LineColor::get() + { + return m_LineColor; + } + void Equation::LineColor::set(Color value) + { + if (m_LineColor.R != value.R || m_LineColor.G != value.G || m_LineColor.B != value.B || m_LineColor.A != value.A) + { + m_LineColor = value; + RaisePropertyChanged(L"LineColor"); + } + } + + Platform::String ^ Equation::LineColorPropertyName::get() + { + return Platform::StringReference(L"LineColor"); + } + + bool Equation::IsGraphableEquation() + { + return !Expression->IsEmpty() && IsLineEnabled && !HasGraphError; + } +} diff --git a/src/GraphControl/Models/Equation.h b/src/GraphControl/Models/Equation.h new file mode 100644 index 000000000..cc404b7e0 --- /dev/null +++ b/src/GraphControl/Models/Equation.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "Utils.h" +#include + +namespace GraphControl +{ + [Windows::UI::Xaml::Data::Bindable] public ref class Equation sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + Equation(); + + OBSERVABLE_OBJECT(); + OBSERVABLE_NAMED_PROPERTY_RW(Platform::String ^, Expression); + OBSERVABLE_NAMED_PROPERTY_RW(bool, IsLineEnabled); + OBSERVABLE_NAMED_PROPERTY_RW(bool, IsValidated); + OBSERVABLE_NAMED_PROPERTY_RW(bool, HasGraphError); + + property Windows::UI::Color LineColor + { + Windows::UI::Color get(); + void set(Windows::UI::Color value); + } + + static property Platform::String + ^ LineColorPropertyName { Platform::String ^ get(); } + + public : Platform::String + ^ GetRequest(); + + bool IsGraphableEquation(); + + private: + std::wstring GetExpression(); + + private: + Windows::UI::Color m_LineColor; + }; +} diff --git a/src/GraphControl/Models/EquationCollection.h b/src/GraphControl/Models/EquationCollection.h new file mode 100644 index 000000000..d1e1ba185 --- /dev/null +++ b/src/GraphControl/Models/EquationCollection.h @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Equation.h" + +namespace GraphControl +{ + delegate void EquationChangedEventHandler(Equation ^ sender); + delegate void VisibilityChangedEventHandler(Equation ^ sender); + +public + ref class EquationCollection sealed : public Windows::Foundation::Collections::IObservableVector + { + public: + virtual ~EquationCollection() + { + } + +#pragma region IIterable + virtual Windows::Foundation::Collections::IIterator< GraphControl::Equation^ >^ First() + { + return m_vector->First(); + } +#pragma endregion + +#pragma region IVector + virtual property unsigned int Size + { + unsigned int get() + { + return m_vector->Size; + } + } + + virtual void Append(GraphControl::Equation ^ value) + { + m_vector->Append(value); + m_tokens.emplace_back( + value->PropertyChanged += ref new Windows::UI::Xaml::Data::PropertyChangedEventHandler(this, &EquationCollection::OnEquationPropertyChanged)); + } + + virtual void Clear() + { + auto numEqs = m_vector->Size; + for (auto i = 0u; i < numEqs; i++) + { + m_vector->GetAt(i)->PropertyChanged -= m_tokens[i]; + } + + m_vector->Clear(); + m_tokens.clear(); + } + + virtual GraphControl::Equation + ^ GetAt(unsigned int index) { return m_vector->GetAt(index); } + + virtual unsigned int GetMany(unsigned int startIndex, Platform::WriteOnlyArray ^ items) + { + return m_vector->GetMany(startIndex, items); + } + + virtual Windows::Foundation::Collections::IVectorView< GraphControl::Equation^ >^ GetView() + { + return m_vector->GetView(); + } + + virtual Platform::Boolean IndexOf(GraphControl::Equation^ value, unsigned int *index) + { + return m_vector->IndexOf(value, index); + } + + virtual void InsertAt(unsigned int index, GraphControl::Equation ^ value) + { + m_vector->InsertAt(index, value); + m_tokens.insert( + m_tokens.begin() + index, + value->PropertyChanged += ref new Windows::UI::Xaml::Data::PropertyChangedEventHandler(this, &EquationCollection::OnEquationPropertyChanged)); + } + + virtual void RemoveAt(unsigned int index) + { + m_vector->GetAt(index)->PropertyChanged -= m_tokens[index]; + + m_vector->RemoveAt(index); + m_tokens.erase(m_tokens.begin() + index); + } + + virtual void RemoveAtEnd() + { + auto size = m_vector->Size; + if (size > 0) + { + m_vector->GetAt(size - 1)->PropertyChanged -= *m_tokens.rbegin(); + m_tokens.erase(m_tokens.end() - 1); + } + + m_vector->RemoveAtEnd(); + } + + virtual void ReplaceAll(const Platform::Array ^ items) + { + auto size = m_vector->Size; + for (auto i = 0u; i < size; i++) + { + m_vector->GetAt(i)->PropertyChanged -= m_tokens[i]; + } + + size = items->Length; + m_tokens.resize(size); + for (auto i = 0u; i < size; i++) + { + m_tokens[i] = items[i]->PropertyChanged += + ref new Windows::UI::Xaml::Data::PropertyChangedEventHandler(this, &EquationCollection::OnEquationPropertyChanged); + } + + m_vector->ReplaceAll(items); + } + + virtual void SetAt(unsigned int index, GraphControl::Equation ^ value) + { + m_vector->GetAt(index)->PropertyChanged -= m_tokens[index]; + + m_vector->SetAt(index, value); + m_tokens[index] = value->PropertyChanged += + ref new Windows::UI::Xaml::Data::PropertyChangedEventHandler(this, &EquationCollection::OnEquationPropertyChanged); + } +#pragma endregion + +#pragma region IObservableVector + virtual event Windows::Foundation::Collections::VectorChangedEventHandler< GraphControl::Equation^ >^ VectorChanged + { + Windows::Foundation::EventRegistrationToken add(Windows::Foundation::Collections::VectorChangedEventHandler< GraphControl::Equation^ >^ handler) + { + return m_vector->VectorChanged += handler; + } + + void remove(Windows::Foundation::EventRegistrationToken token) + { + m_vector->VectorChanged -= token; + } + } +#pragma endregion + + internal: + EquationCollection() : + m_vector(ref new Platform::Collections::Vector< GraphControl::Equation^ >()) + { + } + + event EquationChangedEventHandler ^ EquationChanged; + event EquationChangedEventHandler ^ EquationStyleChanged; + event EquationChangedEventHandler ^ EquationLineEnabledChanged; + + private: + void OnEquationPropertyChanged(Object^ sender, Windows::UI::Xaml::Data::PropertyChangedEventArgs ^ args) + { + auto equation = static_cast(sender); + auto propertyName = args->PropertyName; + if (propertyName == GraphControl::Equation::LineColorPropertyName) + { + EquationStyleChanged(equation); + } + else if (propertyName == GraphControl::Equation::ExpressionPropertyName) + { + EquationChanged(equation); + } + else if (propertyName == GraphControl::Equation::IsLineEnabledPropertyName) + { + EquationLineEnabledChanged(equation); + } + } + + private: + Platform::Collections::Vector ^ m_vector; + std::vector m_tokens; + }; +} diff --git a/src/GraphControl/Models/KeyGraphFeaturesInfo.cpp b/src/GraphControl/Models/KeyGraphFeaturesInfo.cpp new file mode 100644 index 000000000..c7552246d --- /dev/null +++ b/src/GraphControl/Models/KeyGraphFeaturesInfo.cpp @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "KeyGraphFeaturesInfo.h" +#include "../../CalcViewModel/GraphingCalculatorEnums.h" + +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI; +using namespace Windows::UI::ViewManagement; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Media; +using namespace GraphControl; +using namespace Graphing; + +IObservableVector ^ KeyGraphFeaturesInfo::ConvertWStringVector(vector inVector) +{ + auto outVector = ref new Vector(); + + for (auto v : inVector) + { + outVector->Append(ref new String(v.c_str())); + } + + return outVector; +} + +IObservableMap ^ KeyGraphFeaturesInfo::ConvertWStringIntMap(map inMap) +{ + Map ^ outMap = ref new Map(); + ; + for (auto m : inMap) + { + outMap->Insert(ref new String(m.first.c_str()), m.second.ToString()); + } + + return outMap; +} + +KeyGraphFeaturesInfo ^ KeyGraphFeaturesInfo::Create(IGraphFunctionAnalysisData data) +{ + auto res = ref new KeyGraphFeaturesInfo(); + res->XIntercept = ref new String(data.Zeros.c_str()); + res->YIntercept = ref new String(data.YIntercept.c_str()); + res->Domain = ref new String(data.Domain.c_str()); + res->Range = ref new String(data.Range.c_str()); + res->Parity = data.Parity; + res->PeriodicityDirection = data.PeriodicityDirection; + res->PeriodicityExpression = ref new String(data.PeriodicityExpression.c_str()); + res->Minima = ConvertWStringVector(data.Minima); + res->Maxima = ConvertWStringVector(data.Maxima); + res->InflectionPoints = ConvertWStringVector(data.InflectionPoints); + res->Monotonicity = ConvertWStringIntMap(data.MonotoneIntervals); + res->VerticalAsymptotes = ConvertWStringVector(data.VerticalAsymptotes); + res->HorizontalAsymptotes = ConvertWStringVector(data.HorizontalAsymptotes); + res->ObliqueAsymptotes = ConvertWStringVector(data.ObliqueAsymptotes); + res->TooComplexFeatures = data.TooComplexFeatures; + res->AnalysisError = CalculatorApp::AnalysisErrorType::NoError; + return res; +} + +KeyGraphFeaturesInfo ^ KeyGraphFeaturesInfo::Create(CalculatorApp::AnalysisErrorType type) +{ + auto res = ref new KeyGraphFeaturesInfo(); + res->AnalysisError = type; + return res; +} diff --git a/src/GraphControl/Models/KeyGraphFeaturesInfo.h b/src/GraphControl/Models/KeyGraphFeaturesInfo.h new file mode 100644 index 000000000..2d389a0e4 --- /dev/null +++ b/src/GraphControl/Models/KeyGraphFeaturesInfo.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "Utils.h" + + +namespace Graphing +{ + struct IGraphFunctionAnalysisData; +} + +namespace CalculatorApp +{ + enum AnalysisErrorType; +} + +namespace GraphControl +{ + +public + ref class KeyGraphFeaturesInfo sealed + { + public: + PROPERTY_R(Platform::String ^, XIntercept); + PROPERTY_R(Platform::String ^, YIntercept); + PROPERTY_R(int, Parity); + PROPERTY_R(int, PeriodicityDirection); + PROPERTY_R(Platform::String ^, PeriodicityExpression); + PROPERTY_R(Windows::Foundation::Collections::IVector ^, Minima); + PROPERTY_R(Windows::Foundation::Collections::IVector ^, Maxima); + PROPERTY_R(Platform::String ^, Domain); + PROPERTY_R(Platform::String ^, Range); + PROPERTY_R(Windows::Foundation::Collections::IVector ^, InflectionPoints); + PROPERTY_R(SINGLE_ARG(Windows::Foundation::Collections::IObservableMap ^), Monotonicity); + PROPERTY_R(Windows::Foundation::Collections::IVector ^, VerticalAsymptotes); + PROPERTY_R(Windows::Foundation::Collections::IVector ^, HorizontalAsymptotes); + PROPERTY_R(Windows::Foundation::Collections::IVector ^, ObliqueAsymptotes); + PROPERTY_R(int, TooComplexFeatures); + PROPERTY_R(int, AnalysisError); + + internal: + static KeyGraphFeaturesInfo ^ Create(Graphing::IGraphFunctionAnalysisData data); + static KeyGraphFeaturesInfo ^ Create(CalculatorApp::AnalysisErrorType type); + + private: + static Windows::Foundation::Collections::IObservableVector ^ ConvertWStringVector(std::vector inVector); + static Windows::Foundation::Collections:: + IObservableMap ^ ConvertWStringIntMap(std::map inMap); + }; +} diff --git a/src/GraphControl/Themes/generic.xaml b/src/GraphControl/Themes/generic.xaml new file mode 100644 index 000000000..6db6fcfca --- /dev/null +++ b/src/GraphControl/Themes/generic.xaml @@ -0,0 +1,13 @@ + + + diff --git a/src/GraphControl/Utils.h b/src/GraphControl/Utils.h new file mode 100644 index 000000000..e29e4bde0 --- /dev/null +++ b/src/GraphControl/Utils.h @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// 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 \ + { \ + t get() \ + { \ + return m_##n; \ + } \ + \ + private: \ + void set(t value) \ + { \ + m_##n = value; \ + } \ + } \ + \ +private: \ + t m_##n; \ + \ +public: + +#define PROPERTY_RW(t, n) \ + property t n \ + { \ + t get() \ + { \ + return m_##n; \ + } \ + void set(t value) \ + { \ + m_##n = value; \ + } \ + } \ + \ +private: \ + t m_##n; \ + \ +public: + +#define OBSERVABLE_PROPERTY_R(t, n) \ + property t n \ + { \ + t get() \ + { \ + return m_##n; \ + } \ + \ + private: \ + void set(t value) \ + { \ + if (m_##n != value) \ + { \ + m_##n = value; \ + RaisePropertyChanged(L#n); \ + } \ + } \ + } \ + \ +private: \ + t m_##n; \ + \ +public: + +#define OBSERVABLE_PROPERTY_RW(t, n) \ + property t n \ + { \ + t get() \ + { \ + return m_##n; \ + } \ + void set(t value) \ + { \ + if (m_##n != value) \ + { \ + m_##n = value; \ + RaisePropertyChanged(L#n); \ + } \ + } \ + } \ + \ +private: \ + t m_##n; \ + \ +public: + +#define OBSERVABLE_NAMED_PROPERTY_R(t, n) \ + OBSERVABLE_PROPERTY_R(t, n) \ + internal: \ + static property Platform::String ^ n##PropertyName \ + { \ + Platform::String ^ get() { return Platform::StringReference(L#n); } \ + } \ + \ +public: + +#define OBSERVABLE_NAMED_PROPERTY_RW(t, n) \ + OBSERVABLE_PROPERTY_RW(t, n) \ + internal: \ + static property Platform::String ^ n##PropertyName \ + { \ + Platform::String ^ get() { return Platform::StringReference(L#n); } \ + } \ + \ +public: + +#define OBSERVABLE_PROPERTY_FIELD(n) m_##n + +// This variant of the observable object is for objects that don't want to react to property changes +#ifndef UNIT_TESTS +#define OBSERVABLE_OBJECT() \ + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler ^ PropertyChanged; \ + internal: \ + void RaisePropertyChanged(Platform::String ^ p) \ + { \ + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(p)); \ + } \ + \ +public: +#else +#define OBSERVABLE_OBJECT() \ + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler ^ PropertyChanged; \ + internal: \ + void RaisePropertyChanged(Platform::String ^ p) \ + { \ + } \ + \ +public: +#endif + +// The callback specified in the macro is a method in the class that will be called every time the object changes +// the callback is supposed to be have a single parameter of type Platform::String^ +#ifndef UNIT_TESTS +#define OBSERVABLE_OBJECT_CALLBACK(c) \ + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler ^ PropertyChanged; \ + internal: \ + void RaisePropertyChanged(Platform::String ^ p) \ + { \ + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(p)); \ + c(p); \ + } \ + \ +public: +#else +#define OBSERVABLE_OBJECT_CALLBACK(c) \ + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler ^ PropertyChanged; \ + internal: \ + void RaisePropertyChanged(Platform::String ^ p) \ + { \ + c(p); \ + } \ + \ +public: +#endif + +// The variable member generated by this macro should not be used in the class code, use the +// property getter instead. +#define COMMAND_FOR_METHOD(p, m) \ + property Windows::UI::Xaml::Input::ICommand^ p {\ + Windows::UI::Xaml::Input::ICommand^ get() {\ + if (!donotuse_##p) {\ + donotuse_##p = CalculatorApp::Common::MakeDelegate(this, &m);\ + } return donotuse_##p; }} private: Windows::UI::Xaml::Input::ICommand^ donotuse_##p; \ + \ +public: + +// Utilities for DependencyProperties +namespace Utils +{ + namespace Details + { + template + struct IsRefClass + { + static const bool value = __is_ref_class(T); + }; + + template + struct RemoveHat + { + typedef T type; + }; + + template + struct RemoveHat + { + typedef T type; + }; + + template + typename std::enable_if::value, T ^>::type MakeDefault() + { + return nullptr; + } + + template + typename std::enable_if::value, T>::type MakeDefault() + { + return T(); + } + + // There's a bug in Xaml in which custom enums are not recognized by the property system/binding + // therefore this template will determine that for enums the type to use to register the + // DependencyProperty is to be Object, for everything else it will use the type + // NOTE: If we are to find more types in which this is broken this template + // will be specialized for those types to return Object + template + struct TypeToUseForDependencyProperty + { + typedef typename std::conditional::value, Platform::Object, T>::type type; + }; + } + + const wchar_t LRE = 0x202a; // Left-to-Right Embedding + const wchar_t PDF = 0x202c; // Pop Directional Formatting + const wchar_t LRO = 0x202d; // Left-to-Right Override + + // Regular DependencyProperty + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyProperty( + _In_ const wchar_t* const name, + _In_ Windows::UI::Xaml::PropertyMetadata^ metadata) + { + typedef typename Details::RemoveHat::type OwnerType; + typedef typename Details::RemoveHat::type ThisPropertyType; + typedef typename Details::TypeToUseForDependencyProperty::type ThisDependencyPropertyType; + + static_assert(Details::IsRefClass::value, "The owner of a DependencyProperty must be a ref class"); + + return Windows::UI::Xaml::DependencyProperty::Register( + Platform::StringReference(name), + ThisDependencyPropertyType::typeid, // Work around bugs in Xaml by using the filtered type + OwnerType::typeid, + metadata); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyProperty(_In_ const wchar_t* const name) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata(Details::MakeDefault())); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyProperty(_In_ const wchar_t* const name, TType defaultValue) + { + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata(defaultValue)); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyWithCallback( + _In_ wchar_t const * const name, + TCallback callback) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata( + Details::MakeDefault(), + ref new Windows::UI::Xaml::PropertyChangedCallback(callback))); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyWithCallback( + _In_ wchar_t const * const name, + TType defaultValue, + TCallback callback) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata( + defaultValue, + ref new Windows::UI::Xaml::PropertyChangedCallback(callback))); + } + + // Attached DependencyProperty + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttached( + _In_ const wchar_t* const name, + _In_ Windows::UI::Xaml::PropertyMetadata^ metadata) + { + typedef typename Details::RemoveHat::type OwnerType; + typedef typename Details::RemoveHat::type ThisPropertyType; + typedef typename Details::TypeToUseForDependencyProperty::type ThisDependencyPropertyType; + + static_assert(Details::IsRefClass::value, "The owner of a DependencyProperty must be a ref class"); + + return Windows::UI::Xaml::DependencyProperty::RegisterAttached( + Platform::StringReference(name), + ThisDependencyPropertyType::typeid, // Work around bugs in Xaml by using the filtered type + OwnerType::typeid, + metadata); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttached(_In_ const wchar_t* const name) + { + typedef typename Details::RemoveHat