// 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 "CalcViewModel/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" 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::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); } 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(); } void GraphingCalculator::OnTracePointChanged(Windows::Foundation::Point newPoint) { wstringstream traceValueString; // TODO: The below precision should ideally be dynamic based on the current scale of the graph. traceValueString << "x=" << fixed << setprecision(1) << newPoint.X << ", y=" << 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(); } 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"

" << resourceLoader->GetString(L"GraphImageAltText")->Data() << "

"; 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 << L"
"; equationHtml << EscapeHtmlSpecialCharacters(expression)->Data(); 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) { GraphingControl->Focus(::FocusState::Programmatic); m_activeTracingKeyUpToken = Window::Current->CoreWindow->KeyUp += ref new Windows::Foundation::TypedEventHandler( this, &CalculatorApp::GraphingCalculator::ActiveTracing_KeyUp); KeyboardShortcutManager::IgnoreEscape(false); } 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(); } 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; } }