// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #include "pch.h" #include "UnitConverterViewModel.h" #include "CalcManager/Header Files/EngineStrings.h" #include "Common/CalculatorButtonPressedEventArgs.h" #include "Common/CopyPasteManager.h" #include "Common/LocalizationStringUtil.h" #include "Common/LocalizationService.h" #include "Common/LocalizationSettings.h" #include "Common/TraceLogger.h" #include "DataLoaders/CurrencyHttpClient.h" #include "DataLoaders/CurrencyDataLoader.h" #include "DataLoaders/UnitConverterDataLoader.h" using namespace CalculatorApp; using namespace CalculatorApp::Common; using namespace CalculatorApp::Common::Automation; using namespace CalculatorApp::ViewModel; using namespace concurrency; using namespace Platform; using namespace Platform::Collections; using namespace std; using namespace Windows::Foundation; using namespace Windows::Globalization::NumberFormatting; using namespace Windows::System; using namespace Windows::System::Threading; using namespace Windows::System::UserProfile; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Automation::Peers; using namespace Windows::ApplicationModel::Resources; using namespace Windows::Storage; constexpr int EXPECTEDVIEWMODELDATATOKENS = 8; // interval is in 100 nanosecond units constexpr unsigned int TIMER_INTERVAL_IN_MS = 10000; #ifdef UNIT_TESTS #define TIMER_CALLBACK_CONTEXT CallbackContext::Any #else #define TIMER_CALLBACK_CONTEXT CallbackContext::Same #endif const TimeSpan SUPPLEMENTARY_VALUES_INTERVAL = { 10 * TIMER_INTERVAL_IN_MS }; static Unit ^ EMPTY_UNIT = ref new Unit(UCM::EMPTY_UNIT); constexpr size_t UNIT_LIST = 0; constexpr size_t SELECTED_SOURCE_UNIT = 1; constexpr size_t SELECTED_TARGET_UNIT = 2; // x millisecond delay before we consider conversion to be final constexpr unsigned int CONVERSION_FINALIZED_DELAY_IN_MS = 1000; const wregex regexTrimSpacesStart = wregex(L"^\\s+"); const wregex regexTrimSpacesEnd = wregex(L"\\s+$"); namespace { StringReference CurrentCategoryPropertyName(L"CurrentCategory"); StringReference Unit1AutomationNamePropertyName(L"Unit1AutomationName"); StringReference Unit2AutomationNamePropertyName(L"Unit2AutomationName"); StringReference Unit1PropertyName(L"Unit1"); StringReference Unit2PropertyName(L"Unit2"); StringReference Value1PropertyName(L"Value1"); StringReference Value2PropertyName(L"Value2"); StringReference Value1ActivePropertyName(L"Value1Active"); StringReference Value2ActivePropertyName(L"Value2Active"); StringReference Value1AutomationNamePropertyName(L"Value1AutomationName"); StringReference Value2AutomationNamePropertyName(L"Value2AutomationName"); StringReference CurrencySymbol1PropertyName(L"CurrencySymbol1"); StringReference CurrencySymbol2PropertyName(L"CurrencySymbol2"); StringReference CurrencySymbolVisibilityPropertyName(L"CurrencySymbolVisibility"); StringReference SupplementaryVisibilityPropertyName(L"SupplementaryVisibility"); } namespace CalculatorApp::ViewModel::UnitConverterResourceKeys { StringReference ValueFromFormat(L"Format_ValueFrom"); StringReference ValueFromDecimalFormat(L"Format_ValueFrom_Decimal"); StringReference ValueToFormat(L"Format_ValueTo"); StringReference ConversionResultFormat(L"Format_ConversionResult"); StringReference InputUnit_Name(L"InputUnit_Name"); StringReference OutputUnit_Name(L"OutputUnit_Name"); StringReference MaxDigitsReachedFormat(L"Format_MaxDigitsReached"); StringReference UpdatingCurrencyRates(L"UpdatingCurrencyRates"); StringReference CurrencyRatesUpdated(L"CurrencyRatesUpdated"); StringReference CurrencyRatesUpdateFailed(L"CurrencyRatesUpdateFailed"); } UnitConverterViewModel::UnitConverterViewModel(const shared_ptr& model) : m_model(model) , m_resettingTimer(false) , m_value1cp(ConversionParameter::Source) , m_Value1Active(true) , m_Value2Active(false) , m_Value1("0") , m_Value2("0") , m_valueToUnlocalized(L"0") , m_valueFromUnlocalized(L"0") , m_relocalizeStringOnSwitch(false) , m_Categories(ref new Vector()) , m_Units(ref new Vector()) , m_SupplementaryResults(ref new Vector) , m_IsDropDownOpen(false) , m_IsDropDownEnabled(true) , m_IsCurrencyLoadingVisible(false) , m_isCurrencyDataLoaded(false) , m_lastAnnouncedFrom(L"") , m_lastAnnouncedTo(L"") , m_lastAnnouncedConversionResult(L"") , m_isValue1Updating(false) , m_isValue2Updating(false) , m_Announcement(nullptr) , m_Mode(ViewMode::None) , m_CurrencySymbol1(L"") , m_CurrencySymbol2(L"") , m_IsCurrencyCurrentCategory(false) , m_CurrencyRatioEquality(L"") , m_CurrencyRatioEqualityAutomationName(L"") , m_isInputBlocked(false) , m_CurrencyDataLoadFailed(false) { auto localizationService = LocalizationService::GetInstance(); m_model->SetViewModelCallback(make_shared(this)); m_model->SetViewModelCurrencyCallback(make_shared(this)); m_decimalFormatter = localizationService->GetRegionalSettingsAwareDecimalFormatter(); m_decimalFormatter->FractionDigits = 0; m_decimalFormatter->IsGrouped = true; m_decimalSeparator = LocalizationSettings::GetInstance().GetDecimalSeparator(); m_currencyFormatter = localizationService->GetRegionalSettingsAwareCurrencyFormatter(); m_currencyFormatter->IsGrouped = true; m_currencyFormatter->Mode = CurrencyFormatterMode::UseCurrencyCode; m_currencyFormatter->ApplyRoundingForCurrency(RoundingAlgorithm::RoundHalfDown); m_currencyMaxFractionDigits = m_currencyFormatter->FractionDigits; auto resourceLoader = AppResourceProvider::GetInstance(); m_localizedValueFromFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ValueFromFormat); m_localizedValueToFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ValueToFormat); m_localizedConversionResultFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ConversionResultFormat); m_localizedValueFromDecimalFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ValueFromDecimalFormat); m_localizedInputUnitName = resourceLoader->GetResourceString(UnitConverterResourceKeys::InputUnit_Name); m_localizedOutputUnitName = resourceLoader->GetResourceString(UnitConverterResourceKeys::OutputUnit_Name); Unit1AutomationName = m_localizedInputUnitName; Unit2AutomationName = m_localizedOutputUnitName; IsDecimalEnabled = true; m_model->Initialize(); PopulateData(); } void UnitConverterViewModel::ResetView() { m_model->SendCommand(UCM::Command::Reset); OnCategoryChanged(nullptr); } void UnitConverterViewModel::PopulateData() { InitializeView(); } void UnitConverterViewModel::OnCategoryChanged(Object ^ parameter) { m_model->SendCommand(UCM::Command::Clear); ResetCategory(); } void UnitConverterViewModel::ResetCategory() { UCM::Category currentCategory = CurrentCategory->GetModelCategory(); IsCurrencyCurrentCategory = currentCategory.id == NavCategory::Serialize(ViewMode::Currency); m_isInputBlocked = false; SetSelectedUnits(); IsCurrencyLoadingVisible = m_IsCurrencyCurrentCategory && !m_isCurrencyDataLoaded; IsDropDownEnabled = m_Units->GetAt(0) != EMPTY_UNIT; UnitChanged->Execute(nullptr); } void UnitConverterViewModel::SetSelectedUnits() { UCM::CategorySelectionInitializer categoryInitializer = m_model->SetCurrentCategory(CurrentCategory->GetModelCategory()); BuildUnitList(get(categoryInitializer)); UnitFrom = FindUnitInList(get(categoryInitializer)); UnitTo = FindUnitInList(get(categoryInitializer)); } void UnitConverterViewModel::BuildUnitList(const vector& modelUnitList) { m_Units->Clear(); for (const UCM::Unit& modelUnit : modelUnitList) { if (!modelUnit.isWhimsical) { m_Units->Append(ref new Unit(modelUnit)); } } if (m_Units->Size == 0) { m_Units->Append(EMPTY_UNIT); } } Unit ^ UnitConverterViewModel::FindUnitInList(UCM::Unit target) { for (Unit ^ vmUnit : m_Units) { UCM::Unit modelUnit = vmUnit->GetModelUnit(); if (modelUnit.id == target.id) { return vmUnit; } } return EMPTY_UNIT; } void UnitConverterViewModel::OnUnitChanged(Object ^ parameter) { if ((m_Unit1 == nullptr) || (m_Unit2 == nullptr)) { // Return if both Unit1 & Unit2 are not set return; } m_model->SetCurrentUnitTypes(UnitFrom->GetModelUnit(), UnitTo->GetModelUnit()); if (m_supplementaryResultsTimer != nullptr) { // End timer to show results immediately m_supplementaryResultsTimer->Cancel(); } SaveUserPreferences(); } void UnitConverterViewModel::OnSwitchActive(Platform::Object ^ unused) { // this can be false if this switch occurs without the user having explicitly updated any strings // (for example, during deserialization). We only want to try this cleanup if there's actually // something to clean up. if (m_relocalizeStringOnSwitch) { // clean up any ill-formed strings that were in progress before the switch ValueFrom = ConvertToLocalizedString(m_valueFromUnlocalized, false); } SwitchConversionParameters(); // Now deactivate the other if (m_value1cp == ConversionParameter::Source) { Value2Active = false; } else { Value1Active = false; } m_valueFromUnlocalized.swap(m_valueToUnlocalized); Utils::Swap(&m_localizedValueFromFormat, &m_localizedValueToFormat); Utils::Swap(&m_Unit1AutomationName, &m_Unit2AutomationName); RaisePropertyChanged(Unit1AutomationNamePropertyName); RaisePropertyChanged(Unit2AutomationNamePropertyName); m_isInputBlocked = false; m_model->SwitchActive(m_valueFromUnlocalized); } String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings) { Platform::String ^ result; if (stringToLocalize.empty()) { return result; } m_decimalFormatter->IsDecimalPointAlwaysDisplayed = false; m_decimalFormatter->FractionDigits = 0; m_currencyFormatter->IsDecimalPointAlwaysDisplayed = false; m_currencyFormatter->FractionDigits = 0; wstring::size_type posOfE = stringToLocalize.find(L'e'); if (posOfE != wstring::npos) { wstring::size_type posOfSign = posOfE + 1; wchar_t signOfE = stringToLocalize.at(posOfSign); std::wstring significandStr(stringToLocalize.substr(0, posOfE)); std::wstring exponentStr(stringToLocalize.substr(posOfSign + 1, stringToLocalize.length() - posOfSign)); result += ConvertToLocalizedString(significandStr, allowPartialStrings) + "e" + signOfE + ConvertToLocalizedString(exponentStr, allowPartialStrings); } else { // stringToLocalize is in en-US and has the default decimal separator, so this is safe to do. wstring::size_type posOfDecimal = stringToLocalize.find(L'.'); bool hasDecimal = wstring::npos != posOfDecimal; if (hasDecimal) { if (allowPartialStrings) { // allow "in progress" strings, like "3." that occur during the composition of // a final number. Without this, when typing the three characters in "3.2" // you don't see the decimal point when typing it, you only see it once you've finally // typed a post-decimal digit. m_decimalFormatter->IsDecimalPointAlwaysDisplayed = true; m_currencyFormatter->IsDecimalPointAlwaysDisplayed = true; } // force post-decimal digits so that trailing zeroes entered by the user aren't suddenly cut off. m_decimalFormatter->FractionDigits = static_cast(stringToLocalize.length() - (posOfDecimal + 1)); m_currencyFormatter->FractionDigits = m_currencyMaxFractionDigits; } if (IsCurrencyCurrentCategory) { wstring currencyResult = m_currencyFormatter->Format(stod(stringToLocalize))->Data(); wstring currencyCode = m_currencyFormatter->Currency->Data(); // CurrencyFormatter always includes LangCode or Symbol. Make it include LangCode // because this includes a non-breaking space. Remove the LangCode. auto pos = currencyResult.find(currencyCode); if (pos != wstring::npos) { currencyResult.erase(pos, currencyCode.length()); std::wsmatch sm; if (regex_search(currencyResult, sm, regexTrimSpacesStart)) { currencyResult.erase(sm.prefix().length(), sm.length()); } if (regex_search(currencyResult, sm, regexTrimSpacesEnd)) { currencyResult.erase(sm.prefix().length(), sm.length()); } } result = ref new String(currencyResult.c_str()); } else { // Convert the input string to double using stod // Then use the decimalFormatter to reformat the double to Platform String result = m_decimalFormatter->Format(stod(stringToLocalize)); } if (hasDecimal) { // Since the output from GetLocaleInfoEx() and DecimalFormatter are differing for decimal string // we are adding the below work-around of editing the string returned by DecimalFormatter // and replacing the decimal separator with the one returned by GetLocaleInfoEx() String ^ formattedSampleString = m_decimalFormatter->Format(stod("1.1")); wstring formattedSampleWString = wstring(formattedSampleString->Data()); wstring resultWithDecimal = wstring(result->Data()); size_t pos = resultWithDecimal.find(formattedSampleWString[1], 0); if (pos != wstring::npos) { resultWithDecimal.replace(pos, 1, &m_decimalSeparator); } // Copy back the edited string to the result result = ref new String(resultWithDecimal.c_str()); } } wstring resultHolder = wstring(result->Data()); if ((stringToLocalize.front() == L'-' && stod(stringToLocalize) == 0) || resultHolder.back() == L'-') { if (resultHolder.back() == L'-') { result = ref new String(resultHolder.erase(resultHolder.size() - 1, 1).c_str()); } result = L"-" + result; } result = Utils::LRE + result + Utils::PDF; return result; } void UnitConverterViewModel::DisplayPasteError() { String ^ errorMsg = AppResourceProvider::GetInstance()->GetCEngineString(StringReference(SIDS_DOMAIN)); /*SIDS_DOMAIN is for "invalid input"*/ Value1 = errorMsg; Value2 = errorMsg; m_relocalizeStringOnSwitch = false; } void UnitConverterViewModel::UpdateDisplay(const wstring& from, const wstring& to) { String ^ fromStr = this->ConvertToLocalizedString(from, true); UpdateInputBlocked(from); String ^ toStr = this->ConvertToLocalizedString(to, true); bool updatedValueFrom = ValueFrom != fromStr; bool updatedValueTo = ValueTo != toStr; if (updatedValueFrom) { m_valueFromUnlocalized = from; // once we've updated the unlocalized from string, we'll potentially need to clean it back up when switching between fields // to eliminate dangling decimal points. m_relocalizeStringOnSwitch = true; } if (updatedValueTo) { // This is supposed to use trimming logic, but that's highly dependent // on the auto-scaling textbox control which we dont have yet. For now, // not doing anything. It will have to be integrated once that control is // created. m_valueToUnlocalized = to; } m_isValue1Updating = m_Value1Active ? updatedValueFrom : updatedValueTo; m_isValue2Updating = m_Value2Active ? updatedValueFrom : updatedValueTo; // Setting these properties before setting the member variables above causes // a chain of properties that can result in the wrong result being announced // to Narrator. We need to know which values are updating before setting the // below properties, so that we know when to announce the result. if (updatedValueFrom) { ValueFrom = fromStr; } if (updatedValueTo) { ValueTo = toStr; } } void UnitConverterViewModel::UpdateSupplementaryResults(const std::vector>& suggestedValues) { m_cacheMutex.lock(); m_cachedSuggestedValues = suggestedValues; m_cacheMutex.unlock(); // If we're already "ticking", reset the timer if (m_supplementaryResultsTimer != nullptr) { m_resettingTimer = true; m_supplementaryResultsTimer->Cancel(); m_resettingTimer = false; } // Schedule the timer m_supplementaryResultsTimer = ThreadPoolTimer::CreateTimer( ref new TimerElapsedHandler(this, &UnitConverterViewModel::SupplementaryResultsTimerTick, TIMER_CALLBACK_CONTEXT), SUPPLEMENTARY_VALUES_INTERVAL, ref new TimerDestroyedHandler(this, &UnitConverterViewModel::SupplementaryResultsTimerCancel, TIMER_CALLBACK_CONTEXT)); } void UnitConverterViewModel::OnValueActivated(IActivatable ^ control) { control->IsActive = true; } void UnitConverterViewModel::OnButtonPressed(Platform::Object ^ parameter) { NumbersAndOperatorsEnum numOpEnum = CalculatorButtonPressedEventArgs::GetOperationFromCommandParameter(parameter); UCM::Command command = CommandFromButtonId(numOpEnum); // Don't clear the display if combo box is open and escape is pressed if (command == UCM::Command::Clear && IsDropDownOpen) { return; } static const vector OPERANDS = { UCM::Command::Zero, UCM::Command::One, UCM::Command::Two, UCM::Command::Three, UCM::Command::Four, UCM::Command::Five, UCM::Command::Six, UCM::Command::Seven, UCM::Command::Eight, UCM::Command::Nine }; if (find(begin(OPERANDS), end(OPERANDS), command) != OPERANDS.end()) { if (m_isInputBlocked) { return; } if (m_IsCurrencyCurrentCategory) { StartConversionResultTimer(); } } m_model->SendCommand(command); TraceLogger::GetInstance().LogConverterInputReceived(Mode); } void UnitConverterViewModel::OnCopyCommand(Platform::Object ^ parameter) { // EventWriteClipboardCopy_Start(); CopyPasteManager::CopyToClipboard(ref new Platform::String(m_valueFromUnlocalized.c_str())); // EventWriteClipboardCopy_Stop(); } void UnitConverterViewModel::OnPasteCommand(Platform::Object ^ parameter) { // if there's nothing to copy early out if (!CopyPasteManager::HasStringToPaste()) { return; } // Ensure that the paste happens on the UI thread // EventWriteClipboardPaste_Start(); // Any converter ViewMode is fine here. auto that(this); create_task(CopyPasteManager::GetStringToPaste(m_Mode, NavCategory::GetGroupType(m_Mode), -1, BitLength::BitLengthUnknown)) .then([that](String ^ pastedString) { that->OnPaste(pastedString); }, concurrency::task_continuation_context::use_current()); } void UnitConverterViewModel::InitializeView() { vector categories = m_model->GetCategories(); for (UINT i = 0; i < categories.size(); i++) { Category ^ category = ref new Category(categories[i]); m_Categories->Append(category); } RestoreUserPreferences(); CurrentCategory = ref new Category(m_model->GetCurrentCategory()); } void UnitConverterViewModel::OnPropertyChanged(Platform::String ^ prop) { static bool isCategoryChanging = false; if (prop == CurrentCategoryPropertyName) { isCategoryChanging = true; CategoryChanged->Execute(nullptr); isCategoryChanging = false; } else if (prop == Unit1PropertyName || prop == Unit2PropertyName) { // Category changes will handle updating units after they've both been updated. // This event should only be used to update units from explicit user interaction. if (!isCategoryChanging) { UnitChanged->Execute(nullptr); } // Get the localized automation name for each CalculationResults field if (prop == Unit1PropertyName) { UpdateValue1AutomationName(); } else { UpdateValue2AutomationName(); } } else if (prop == Value1PropertyName) { UpdateValue1AutomationName(); } else if (prop == Value2PropertyName) { UpdateValue2AutomationName(); } else if (prop == Value1ActivePropertyName || prop == Value2ActivePropertyName) { // if one of the values is activated, and as a result both are true, it means // that we're trying to switch. if (Value1Active && Value2Active) { SwitchActive->Execute(nullptr); } UpdateValue1AutomationName(); UpdateValue2AutomationName(); } else if (prop == SupplementaryResultsPropertyName) { RaisePropertyChanged(SupplementaryVisibilityPropertyName); } else if (prop == Value1AutomationNamePropertyName) { m_isValue1Updating = false; if (!m_isValue2Updating) { AnnounceConversionResult(); } } else if (prop == Value2AutomationNamePropertyName) { m_isValue2Updating = false; if (!m_isValue1Updating) { AnnounceConversionResult(); } } else if (prop == CurrencySymbol1PropertyName || prop == CurrencySymbol2PropertyName) { RaisePropertyChanged(CurrencySymbolVisibilityPropertyName); } } // Saving User Preferences of Category and Associated-Units across Sessions. void UnitConverterViewModel::SaveUserPreferences() { if (UnitsAreValid()) { ApplicationDataContainer ^ localSettings = ApplicationData::Current->LocalSettings; if (!m_IsCurrencyCurrentCategory) { auto userPreferences = m_model->SaveUserPreferences(); localSettings->Values->Insert(ref new String(L"UnitConverterPreferences"), ref new String(userPreferences.c_str())); } else { // Currency preferences shouldn't be saved in the same way as standard converter modes because // the delay loading creates a big mess of issues that are better to avoid. localSettings->Values->Insert(UnitConverterResourceKeys::CurrencyUnitFromKey, UnitFrom->Abbreviation); localSettings->Values->Insert(UnitConverterResourceKeys::CurrencyUnitToKey, UnitTo->Abbreviation); } } } // Restoring User Preferences of Category and Associated-Units. void UnitConverterViewModel::RestoreUserPreferences() { if (!IsCurrencyCurrentCategory) { ApplicationDataContainer ^ localSettings = ApplicationData::Current->LocalSettings; if (localSettings->Values->HasKey(ref new String(L"UnitConverterPreferences"))) { String ^ userPreferences = safe_cast(localSettings->Values->Lookup(ref new String(L"UnitConverterPreferences"))); m_model->RestoreUserPreferences(userPreferences->Data()); } } } void UnitConverterViewModel::OnCurrencyDataLoadFinished(bool didLoad) { m_isCurrencyDataLoaded = true; CurrencyDataLoadFailed = !didLoad; m_model->ResetCategoriesAndRatios(); m_model->Calculate(); ResetCategory(); StringReference key = didLoad ? UnitConverterResourceKeys::CurrencyRatesUpdated : UnitConverterResourceKeys::CurrencyRatesUpdateFailed; String ^ announcement = AppResourceProvider::GetInstance()->GetResourceString(key); Announcement = CalculatorAnnouncement::GetUpdateCurrencyRatesAnnouncement(announcement); } void UnitConverterViewModel::OnCurrencyTimestampUpdated(_In_ const wstring& timestamp, bool isWeekOld) { CurrencyDataIsWeekOld = isWeekOld; CurrencyTimestamp = ref new String(timestamp.c_str()); } void UnitConverterViewModel::RefreshCurrencyRatios() { m_isCurrencyDataLoaded = false; IsCurrencyLoadingVisible = true; String ^ announcement = AppResourceProvider::GetInstance()->GetResourceString(UnitConverterResourceKeys::UpdatingCurrencyRates); Announcement = CalculatorAnnouncement::GetUpdateCurrencyRatesAnnouncement(announcement); auto that(this); auto refreshTask = create_task([that] { return that->m_model->RefreshCurrencyRatios().get(); }); refreshTask.then( [that](const pair& refreshResult) { bool didLoad = refreshResult.first; wstring timestamp = refreshResult.second; that->OnCurrencyTimestampUpdated(timestamp, false /*isWeekOldData*/); that->OnCurrencyDataLoadFinished(didLoad); }, task_continuation_context::use_current()); } void UnitConverterViewModel::OnNetworkBehaviorChanged(_In_ NetworkAccessBehavior newBehavior) { CurrencyDataLoadFailed = false; NetworkBehavior = newBehavior; } UnitConversionManager::Command UnitConverterViewModel::CommandFromButtonId(NumbersAndOperatorsEnum button) { UCM::Command command; switch (button) { case NumbersAndOperatorsEnum::Zero: command = UCM::Command::Zero; break; case NumbersAndOperatorsEnum::One: command = UCM::Command::One; break; case NumbersAndOperatorsEnum::Two: command = UCM::Command::Two; break; case NumbersAndOperatorsEnum::Three: command = UCM::Command::Three; break; case NumbersAndOperatorsEnum::Four: command = UCM::Command::Four; break; case NumbersAndOperatorsEnum::Five: command = UCM::Command::Five; break; case NumbersAndOperatorsEnum::Six: command = UCM::Command::Six; break; case NumbersAndOperatorsEnum::Seven: command = UCM::Command::Seven; break; case NumbersAndOperatorsEnum::Eight: command = UCM::Command::Eight; break; case NumbersAndOperatorsEnum::Nine: command = UCM::Command::Nine; break; case NumbersAndOperatorsEnum::Decimal: command = UCM::Command::Decimal; break; case NumbersAndOperatorsEnum::Negate: command = UCM::Command::Negate; break; case NumbersAndOperatorsEnum::Backspace: command = UCM::Command::Backspace; break; case NumbersAndOperatorsEnum::Clear: command = UCM::Command::Clear; break; default: command = UCM::Command::None; break; } return command; } void UnitConverterViewModel::SupplementaryResultsTimerTick(ThreadPoolTimer ^ timer) { timer->Cancel(); } void UnitConverterViewModel::SupplementaryResultsTimerCancel(ThreadPoolTimer ^ timer) { if (!m_resettingTimer) { RefreshSupplementaryResults(); } } void UnitConverterViewModel::RefreshSupplementaryResults() { m_cacheMutex.lock(); m_SupplementaryResults->Clear(); vector whimsicals; for (tuple suggestedValue : m_cachedSuggestedValues) { SupplementaryResult ^ result = ref new SupplementaryResult(this->ConvertToLocalizedString(get<0>(suggestedValue), false), ref new Unit(get<1>(suggestedValue))); if (result->IsWhimsical()) { whimsicals.push_back(result); } else { m_SupplementaryResults->Append(result); } } if (whimsicals.size() > 0) { m_SupplementaryResults->Append(whimsicals[0]); } m_cacheMutex.unlock(); RaisePropertyChanged(SupplementaryResultsPropertyName); // EventWriteConverterSupplementaryResultsUpdated(); } // When UpdateDisplay is called, the ViewModel will remember the From/To unlocalized display values // This function will announce the conversion result after the ValueTo/ValueFrom automation names update, // only if the new unlocalized display values are different from the last announced values, and if the // values are not both zero. void UnitConverterViewModel::AnnounceConversionResult() { if ((m_valueFromUnlocalized != m_lastAnnouncedFrom || m_valueToUnlocalized != m_lastAnnouncedTo) && Unit1 != nullptr && Unit2 != nullptr) { m_lastAnnouncedFrom = m_valueFromUnlocalized; m_lastAnnouncedTo = m_valueToUnlocalized; Unit ^ unitFrom = Value1Active ? Unit1 : Unit2; Unit ^ unitTo = (unitFrom == Unit1) ? Unit2 : Unit1; m_lastAnnouncedConversionResult = GetLocalizedConversionResultStringFormat(ValueFrom, unitFrom->Name, ValueTo, unitTo->Name); Announcement = CalculatorAnnouncement::GetDisplayUpdatedAnnouncement(m_lastAnnouncedConversionResult); } } void UnitConverterViewModel::UpdateInputBlocked(_In_ const wstring& currencyInput) { // currencyInput is in en-US and has the default decimal separator, so this is safe to do. auto posOfDecimal = currencyInput.find(L'.'); m_isInputBlocked = false; if (posOfDecimal != wstring::npos && IsCurrencyCurrentCategory) { m_isInputBlocked = (posOfDecimal + static_cast(m_currencyMaxFractionDigits) + 1 == currencyInput.length()); } } NumbersAndOperatorsEnum UnitConverterViewModel::MapCharacterToButtonId(const wchar_t ch, bool& canSendNegate) { static_assert(NumbersAndOperatorsEnum::Zero < NumbersAndOperatorsEnum::One, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::One < NumbersAndOperatorsEnum::Two, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Two < NumbersAndOperatorsEnum::Three, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Three < NumbersAndOperatorsEnum::Four, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Four < NumbersAndOperatorsEnum::Five, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Five < NumbersAndOperatorsEnum::Six, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Six < NumbersAndOperatorsEnum::Seven, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Seven < NumbersAndOperatorsEnum::Eight, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Eight < NumbersAndOperatorsEnum::Nine, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Zero < NumbersAndOperatorsEnum::Nine, "NumbersAndOperatorsEnum order is invalid"); NumbersAndOperatorsEnum mappedValue = NumbersAndOperatorsEnum::None; canSendNegate = false; switch (ch) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': mappedValue = NumbersAndOperatorsEnum::Zero + static_cast(ch - L'0'); canSendNegate = true; break; case '-': mappedValue = NumbersAndOperatorsEnum::Negate; break; default: // Respect the user setting for decimal separator if (ch == m_decimalSeparator) { mappedValue = NumbersAndOperatorsEnum::Decimal; canSendNegate = true; break; } } if (mappedValue == NumbersAndOperatorsEnum::None) { if (LocalizationSettings::GetInstance().IsLocalizedDigit(ch)) { mappedValue = NumbersAndOperatorsEnum::Zero + static_cast(ch - LocalizationSettings::GetInstance().GetDigitSymbolFromEnUsDigit(L'0')); canSendNegate = true; } } return mappedValue; } void UnitConverterViewModel::OnPaste(String ^ stringToPaste) { // If pastedString is invalid("NoOp") then display pasteError else process the string if (CopyPasteManager::IsErrorMessage(stringToPaste)) { this->DisplayPasteError(); return; } TraceLogger::GetInstance().LogInputPasted(Mode); bool isFirstLegalChar = true; bool sendNegate = false; wstring accumulation = L""; for (auto it = stringToPaste->Begin(); it != stringToPaste->End(); it++) { bool canSendNegate = false; NumbersAndOperatorsEnum op = MapCharacterToButtonId(*it, canSendNegate); if (NumbersAndOperatorsEnum::None != op) { if (isFirstLegalChar) { // Send Clear before sending something that will actually apply // to the field. m_model->SendCommand(UCM::Command::Clear); isFirstLegalChar = false; // If the very first legal character is a - sign, send negate // after sending the next legal character. Send nothing now, or // it will be ignored. if (NumbersAndOperatorsEnum::Negate == op) { sendNegate = true; } } // Negate is only allowed if it's the first legal character, which is handled above. if (NumbersAndOperatorsEnum::Negate != op) { UCM::Command cmd = CommandFromButtonId(op); m_model->SendCommand(cmd); if (sendNegate) { if (canSendNegate) { m_model->SendCommand(UCM::Command::Negate); } sendNegate = false; } } accumulation += *it; UpdateInputBlocked(accumulation); if (m_isInputBlocked) { break; } } else { sendNegate = false; } } } String ^ UnitConverterViewModel::GetLocalizedAutomationName(_In_ String ^ displayvalue, _In_ String ^ unitname, _In_ String ^ format) { String ^ valueToLocalize = displayvalue; if (displayvalue == ValueFrom && Utils::IsLastCharacterTarget(m_valueFromUnlocalized, m_decimalSeparator)) { // Need to compute a second localized value for the automation // name that does not include the decimal separator. displayvalue = ConvertToLocalizedString(m_valueFromUnlocalized, false /*allowTrailingDecimal*/); format = m_localizedValueFromDecimalFormat; } return LocalizationStringUtil::GetLocalizedString(format, displayvalue, unitname); } String ^ UnitConverterViewModel::GetLocalizedConversionResultStringFormat( _In_ String ^ fromValue, _In_ String ^ fromUnit, _In_ String ^ toValue, _In_ String ^ toUnit) { return LocalizationStringUtil::GetLocalizedString(m_localizedConversionResultFormat, fromValue, fromUnit, toValue, toUnit); } void UnitConverterViewModel::UpdateValue1AutomationName() { if (Unit1) { Value1AutomationName = GetLocalizedAutomationName(Value1, Unit1->AccessibleName, m_localizedValueFromFormat); } } void UnitConverterViewModel::UpdateValue2AutomationName() { if (Unit2) { Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2->AccessibleName, m_localizedValueToFormat); } } void UnitConverterViewModel::OnMaxDigitsReached() { String ^ format = AppResourceProvider::GetInstance()->GetResourceString(UnitConverterResourceKeys::MaxDigitsReachedFormat); auto announcement = LocalizationStringUtil::GetLocalizedString(format, m_lastAnnouncedConversionResult); Announcement = CalculatorAnnouncement::GetMaxDigitsReachedAnnouncement(announcement); } bool UnitConverterViewModel::UnitsAreValid() { return UnitFrom != nullptr && !UnitFrom->Abbreviation->IsEmpty() && UnitTo != nullptr && !UnitTo->Abbreviation->IsEmpty(); } void UnitConverterViewModel::StartConversionResultTimer() { auto that(this); m_conversionResultTaskHelper = make_unique(CONVERSION_FINALIZED_DELAY_IN_MS, [that]() { if (that->UnitsAreValid()) { String ^ valueFrom = that->m_Value1Active ? that->m_Value1 : that->m_Value2; String ^ valueTo = that->m_Value1Active ? that->m_Value2 : that->m_Value1; } }); } String ^ SupplementaryResult::GetLocalizedAutomationName() { auto format = AppResourceProvider::GetInstance()->GetResourceString("SupplementaryUnit_AutomationName"); return LocalizationStringUtil::GetLocalizedString(format, this->Value, this->Unit->Name); }