diff --git a/src/CalcViewModel/UnitConverterViewModel.h b/src/CalcViewModel/UnitConverterViewModel.h index fd62940d..cc6e3a9c 100644 --- a/src/CalcViewModel/UnitConverterViewModel.h +++ b/src/CalcViewModel/UnitConverterViewModel.h @@ -35,6 +35,11 @@ namespace CalculatorApp } } + int GetModelCategoryId() + { + return GetModelCategory().id; + } + internal : const UnitConversionManager::Category& GetModelCategory() const { return m_original; @@ -116,7 +121,7 @@ namespace CalculatorApp OBSERVABLE_PROPERTY_R(CalculatorApp::ViewModel::Unit ^, Unit); }; - interface class IActivatable + public interface class IActivatable { virtual property bool IsActive; }; @@ -232,12 +237,14 @@ namespace CalculatorApp void AnnounceConversionResult(); + void OnPaste(Platform::String ^ stringToPaste); + void RefreshCurrencyRatios(); + void OnValueActivated(IActivatable ^ control); + internal : void ResetView(); void PopulateData(); NumbersAndOperatorsEnum MapCharacterToButtonId(const wchar_t ch, bool& canSendNegate); void DisplayPasteError(); - void OnValueActivated(IActivatable ^ control); - void OnPaste(Platform::String ^ stringToPaste); void OnCopyCommand(Platform::Object ^ parameter); void OnPasteCommand(Platform::Object ^ parameter); @@ -270,7 +277,6 @@ namespace CalculatorApp void OnCurrencyDataLoadFinished(bool didLoad); void OnCurrencyTimestampUpdated(_In_ const std::wstring& timestamp, bool isWeekOld); - void RefreshCurrencyRatios(); void OnNetworkBehaviorChanged(_In_ CalculatorApp::NetworkAccessBehavior newBehavior); const std::wstring& GetValueFromUnlocalized() const diff --git a/src/Calculator/Calculator.csproj b/src/Calculator/Calculator.csproj index 40a0b03b..3fc22df9 100644 --- a/src/Calculator/Calculator.csproj +++ b/src/Calculator/Calculator.csproj @@ -227,6 +227,9 @@ TitleBar.xaml + + UnitConverter.xaml + @@ -631,6 +634,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile @@ -687,6 +694,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + diff --git a/src/Calculator/Views/DelighterUnitStyles.xaml b/src/Calculator/Views/DelighterUnitStyles.xaml new file mode 100644 index 00000000..ed8c376e --- /dev/null +++ b/src/Calculator/Views/DelighterUnitStyles.xaml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/MainPage.xaml b/src/Calculator/Views/MainPage.xaml index cba7d6bc..c0febd59 100644 --- a/src/Calculator/Views/MainPage.xaml +++ b/src/Calculator/Views/MainPage.xaml @@ -20,11 +20,11 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Calculator/Views/UnitConverter.xaml.cs b/src/Calculator/Views/UnitConverter.xaml.cs new file mode 100644 index 00000000..f1847ecf --- /dev/null +++ b/src/Calculator/Views/UnitConverter.xaml.cs @@ -0,0 +1,386 @@ +using CalculatorApp.Common; +using CalculatorApp.Controls; +using CalculatorApp.ViewModel; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.System; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace CalculatorApp +{ + class Activatable : ViewModel.IActivatable + { + public Activatable(Func getter, Action setter) + { + m_getter = getter; + m_setter = setter; + } + + public bool IsActive + { + get => m_getter(); + set => m_setter(value); + } + + private Func m_getter; + private Action m_setter; + } + + public sealed partial class UnitConverter : UserControl + { + public UnitConverter() + { + this.InitializeComponent(); + } + + public Windows.UI.Xaml.HorizontalAlignment FlowDirectionHorizontalAlignment + { + get => this.m_FlowDirectionHorizontalAlignment; + } + + private Windows.UI.Xaml.HorizontalAlignment m_FlowDirectionHorizontalAlignment; + + public void AnimateConverter() + { + if (uiSettings.Value.AnimationsEnabled) + { + AnimationStory.Begin(); + } + } + + public CalculatorApp.ViewModel.UnitConverterViewModel Model + { + get => (CalculatorApp.ViewModel.UnitConverterViewModel)this.DataContext; + } + + public Windows.UI.Xaml.FlowDirection LayoutDirection + { + get => this.m_layoutDirection; + } + + public void SetDefaultFocus() + { + Control[] focusPrecedence = new Control[] { Value1, CurrencyRefreshBlockControl, OfflineBlock, ClearEntryButtonPos0 }; + + foreach (Control control in focusPrecedence) + { + if (control.Focus(FocusState.Programmatic)) + { + break; + } + } + } + + private void OnValueKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Space) + { + OnValueSelected(sender); + } + } + + private void OnContextRequested(UIElement sender, ContextRequestedEventArgs e) + { + OnValueSelected(sender); + var requestedElement = ((FrameworkElement)sender); + + PasteMenuItem.IsEnabled = CopyPasteManager.HasStringToPaste(); + + Point point; + if (e.TryGetPosition(requestedElement, out point)) + { + m_resultsFlyout.ShowAt(requestedElement, point); + } + else + { + // Not invoked via pointer, so let XAML choose a default location. + m_resultsFlyout.ShowAt(requestedElement); + } + + e.Handled = true; + } + + private void OnContextCanceled(UIElement sender, RoutedEventArgs e) + { + m_resultsFlyout.Hide(); + } + + private void OnCopyMenuItemClicked(object sender, RoutedEventArgs e) + { + var calcResult = ((CalculationResult)m_resultsFlyout.Target); + CopyPasteManager.CopyToClipboard(calcResult.GetRawDisplayValue()); + } + + void OnPasteMenuItemClicked(object sender, RoutedEventArgs e) + { + UnitConverter that = this; + _ = Task.Run(async () => + { + string pastedString = await CopyPasteManager.GetStringToPaste(Model.Mode, CategoryGroupType.Converter, NumberBase.Unknown, BitLength.BitLengthUnknown); + that.Model.OnPaste(pastedString); + }); + } + + private void OnValueSelected(object sender) + { + var value = ((CalculationResult)sender); + // update the font size since the font is changed to bold + value.UpdateTextState(); + ((UnitConverterViewModel)this.DataContext).OnValueActivated(new Activatable(() => value.IsActive, flag => value.IsActive = flag)); + } + + private void UpdateDropDownState(object sender, object e) + { + ((UnitConverterViewModel)this.DataContext).IsDropDownOpen = (Units1.IsDropDownOpen) || (Units2.IsDropDownOpen); + KeyboardShortcutManager.UpdateDropDownState((Units1.IsDropDownOpen) || (Units2.IsDropDownOpen)); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + } + + private void CurrencyRefreshButton_Click(object sender, RoutedEventArgs e) + { + // If IsCurrencyLoadingVisible is true that means CurrencyRefreshButton_Click was recently called + // and is still executing. In this case there is no reason to process the click. + if (!Model.IsCurrencyLoadingVisible) + { + if (Model.NetworkBehavior == NetworkAccessBehavior.OptIn) + { + m_meteredConnectionOverride = true; + } + + Model.RefreshCurrencyRatios(); + } + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + string propertyName = e.PropertyName; + if (propertyName == UnitConverterViewModel.NetworkBehaviorPropertyName || propertyName == UnitConverterViewModel.CurrencyDataLoadFailedPropertyName) + { + OnNetworkBehaviorChanged(); + } + else if (propertyName == UnitConverterViewModel.CurrencyDataIsWeekOldPropertyName) + { + SetCurrencyTimestampFontWeight(); + } + else if ( + propertyName == UnitConverterViewModel.IsCurrencyLoadingVisiblePropertyName + || propertyName == UnitConverterViewModel.IsCurrencyCurrentCategoryPropertyName) + { + OnIsDisplayVisibleChanged(); + } + } + + private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + // CSHARP_MIGRATION: TODO: double check the original code with below migrated code + Model.PropertyChanged -= OnPropertyChanged; + Model.PropertyChanged += OnPropertyChanged; + + OnNetworkBehaviorChanged(); + } + + private void OnIsDisplayVisibleChanged() + { + if (!Model.IsCurrencyCurrentCategory) + { + VisualStateManager.GoToState(this, UnitLoadedState.Name, false); + } + else + { + if (Model.IsCurrencyLoadingVisible) + { + VisualStateManager.GoToState(this, UnitNotLoadedState.Name, false); + StartProgressRingWithDelay(); + } + else + { + HideProgressRing(); + VisualStateManager.GoToState(this, !string.IsNullOrEmpty(Model.CurrencyTimestamp) ? UnitLoadedState.Name : UnitNotLoadedState.Name, true); + } + } + } + + private void Units1_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if ((Units1.Visibility == Visibility.Visible) && Units1.IsEnabled) + { + SetDefaultFocus(); + } + } + + private void OnNetworkBehaviorChanged() + { + switch (Model.NetworkBehavior) + { + case NetworkAccessBehavior.Normal: + OnNormalNetworkAccess(); + break; + case NetworkAccessBehavior.OptIn: + OnOptInNetworkAccess(); + break; + case NetworkAccessBehavior.Offline: + OnOfflineNetworkAccess(); + break; + } + } + + private void OnNormalNetworkAccess() + { + CurrencyRefreshBlockControl.Visibility = Visibility.Visible; + OfflineBlock.Visibility = Visibility.Collapsed; + + if (Model.CurrencyDataLoadFailed) + { + SetFailedToRefreshStatus(); + } + else + { + SetNormalCurrencyStatus(); + } + } + + void OnOptInNetworkAccess() + { + CurrencyRefreshBlockControl.Visibility = Visibility.Visible; + OfflineBlock.Visibility = Visibility.Collapsed; + + if (m_meteredConnectionOverride && Model.CurrencyDataLoadFailed) + { + SetFailedToRefreshStatus(); + } + else + { + SetChargesMayApplyStatus(); + } + } + + void OnOfflineNetworkAccess() + { + CurrencyRefreshBlockControl.Visibility = Visibility.Collapsed; + OfflineBlock.Visibility = Visibility.Visible; + } + + private void InitializeOfflineStatusTextBlock() + { + var resProvider = AppResourceProvider.GetInstance(); + string offlineStatusHyperlinkText = resProvider.GetResourceString("OfflineStatusHyperlinkText"); + + // The resource string has the 'NetworkSettings' hyperlink wrapped with '%HL%'. + // Break the string and assign pieces appropriately. + const string delimiter = "%HL%"; + int delimiterLength = delimiter.Length; + + // Find the delimiters. + int firstSplitPosition = offlineStatusHyperlinkText.IndexOf(delimiter); + Debug.Assert(firstSplitPosition != -1); + int secondSplitPosition = offlineStatusHyperlinkText.IndexOf(delimiter, firstSplitPosition + 1); + Debug.Assert(secondSplitPosition != -1); + int hyperlinkTextLength = secondSplitPosition - (firstSplitPosition + delimiterLength); + + // Assign pieces. + var offlineStatusTextBeforeHyperlink = offlineStatusHyperlinkText.Substring(0, firstSplitPosition); + var offlineStatusTextLink = offlineStatusHyperlinkText.Substring(firstSplitPosition + delimiterLength, hyperlinkTextLength); + var offlineStatusTextAfterHyperlink = offlineStatusHyperlinkText.Substring(secondSplitPosition + delimiterLength); + + OfflineRunBeforeLink.Text = offlineStatusTextBeforeHyperlink; + OfflineRunLink.Text = offlineStatusTextLink; + OfflineRunAfterLink.Text = offlineStatusTextAfterHyperlink; + + AutomationProperties.SetName(OfflineBlock, offlineStatusTextBeforeHyperlink + " " + offlineStatusTextLink + " " + offlineStatusTextAfterHyperlink); + } + + private void SetNormalCurrencyStatus() + { + CurrencySecondaryStatus.Text = ""; + } + + private void SetChargesMayApplyStatus() + { + VisualStateManager.GoToState(this, "ChargesMayApplyCurrencyStatus", false); + CurrencySecondaryStatus.Text = m_chargesMayApplyText; + } + + private void SetFailedToRefreshStatus() + { + VisualStateManager.GoToState(this, "FailedCurrencyStatus", false); + CurrencySecondaryStatus.Text = m_failedToRefreshText; + } + + private void SetCurrencyTimestampFontWeight() + { + if (Model.CurrencyDataIsWeekOld) + { + VisualStateManager.GoToState(this, "WeekOldTimestamp", false); + } + else + { + VisualStateManager.GoToState(this, "DefaultTimestamp", false); + } + } + + private void StartProgressRingWithDelay() + { + HideProgressRing(); + + TimeSpan delay = TimeSpan.FromMilliseconds(500); + + m_delayTimer = new DispatcherTimer(); + m_delayTimer.Interval = delay; + m_delayTimer.Tick += OnDelayTimerTick; + + m_delayTimer.Start(); + } + + private void OnDelayTimerTick(object sender, object e) + { + CurrencyLoadingProgressRing.IsActive = true; + m_delayTimer.Stop(); + } + + private void HideProgressRing() + { + if (m_delayTimer != null) + { + m_delayTimer.Stop(); + } + + CurrencyLoadingProgressRing.IsActive = false; + } + + private void SupplementaryResultsPanelInGrid_SizeChanged(object sender, Windows.UI.Xaml.SizeChangedEventArgs e) + { + // We add 0.01 to be sure to not create an infinite loop with SizeChanged events cascading due to float approximation + RowDltrUnits.MinHeight = Math.Max(48.0, e.NewSize.Height + 0.01); + } + + private void OnVisualStateChanged(object sender, Windows.UI.Xaml.VisualStateChangedEventArgs e) + { + var mode = NavCategory.Deserialize(Model.CurrentCategory.GetModelCategoryId()); + TraceLogger.GetInstance().LogVisualStateChanged(mode, e.NewState.Name, false); + } + + private static Lazy uiSettings = new Lazy(true); + private Windows.UI.Xaml.FlowDirection m_layoutDirection; + private Windows.UI.Xaml.Controls.MenuFlyout m_resultsFlyout; + + private string m_chargesMayApplyText; + private string m_failedToRefreshText; + + private bool m_meteredConnectionOverride; + + private Windows.UI.Xaml.DispatcherTimer m_delayTimer; + } +} +