993 lines
36 KiB
C++
993 lines
36 KiB
C++
// 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<UCM::IUnitConverter>& 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<Category ^>())
|
|
, m_Units(ref new Vector<Unit ^>())
|
|
, m_SupplementaryResults(ref new Vector<SupplementaryResult ^>)
|
|
, 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<UnitConverterVMCallback>(this));
|
|
m_model->SetViewModelCurrencyCallback(make_shared<ViewModelCurrencyCallback>(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()
|
|
{
|
|
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<UNIT_LIST>(categoryInitializer));
|
|
|
|
UnitFrom = FindUnitInList(get<SELECTED_SOURCE_UNIT>(categoryInitializer));
|
|
UnitTo = FindUnitInList(get<SELECTED_TARGET_UNIT>(categoryInitializer));
|
|
}
|
|
|
|
void UnitConverterViewModel::BuildUnitList(const vector<UCM::Unit>& 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);
|
|
swap(m_localizedValueFromFormat, m_localizedValueToFormat);
|
|
|
|
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<int>(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<std::tuple<std::wstring, UnitConversionManager::Unit>>& 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 constexpr UCM::Command 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 (m_isInputBlocked)
|
|
{
|
|
return;
|
|
}
|
|
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), NumberBase::Unknown, BitLength::BitLengthUnknown))
|
|
.then([that](String ^ pastedString) { that->OnPaste(pastedString); }, concurrency::task_continuation_context::use_current());
|
|
}
|
|
|
|
void UnitConverterViewModel::InitializeView()
|
|
{
|
|
vector<UCM::Category> 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<String ^>(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<bool, wstring>& 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<SupplementaryResult ^> whimsicals;
|
|
|
|
for (tuple<wstring, UCM::Unit> 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<size_t>(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<NumbersAndOperatorsEnum>(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<NumbersAndOperatorsEnum>(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;
|
|
|
|
for (const auto ch : stringToPaste)
|
|
{
|
|
bool canSendNegate = false;
|
|
|
|
NumbersAndOperatorsEnum op = MapCharacterToButtonId(ch, 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 += ch;
|
|
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();
|
|
}
|
|
|
|
String ^ SupplementaryResult::GetLocalizedAutomationName()
|
|
{
|
|
auto format = AppResourceProvider::GetInstance()->GetResourceString("SupplementaryUnit_AutomationName");
|
|
return LocalizationStringUtil::GetLocalizedString(format, this->Value, this->Unit->Name);
|
|
}
|