From 7847214a5835ee3259044bddf11c7c51978ddb8f Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Thu, 1 Aug 2024 18:17:56 +0100 Subject: [PATCH] Updated Controls Button is now default to Stretch as per standard Wpf Button Update for MessageBox Update for PasswordBox Update for TreeView --- Version.json | 2 +- .../Views/Pages/DashboardPage.xaml | 13 +- .../Controls/Button/Button.xaml | 8 +- .../Controls/MessageBox/MessageBox.cs | 199 ++++++++++-------- .../Controls/PasswordBox/PasswordBox.cs | 134 +++++++++--- .../Controls/TreeView/TreeView.xaml | 64 +++++- .../Controls/TreeView/TreeViewItem.xaml | 39 +--- .../Resources/Variables.xaml | 16 +- src/CrissCross.WPF/CrissCrossWpfDictionary.cs | 31 ++- 9 files changed, 320 insertions(+), 186 deletions(-) diff --git a/Version.json b/Version.json index 07126c2..3944764 100644 --- a/Version.json +++ b/Version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.0.4", + "version": "2.0.5", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/heads/main$" diff --git a/src/CrissCross.WPF.UI.Test/Views/Pages/DashboardPage.xaml b/src/CrissCross.WPF.UI.Test/Views/Pages/DashboardPage.xaml index 9f70917..29524de 100644 --- a/src/CrissCross.WPF.UI.Test/Views/Pages/DashboardPage.xaml +++ b/src/CrissCross.WPF.UI.Test/Views/Pages/DashboardPage.xaml @@ -86,6 +86,17 @@ Grid.Row="3" Grid.Column="2" Height="312" - Source="/Assets/working.gif" AnimateInDesignMode="True" /> + AnimateInDesignMode="True" + Source="/Assets/working.gif" /> + + + + + + + diff --git a/src/CrissCross.WPF.UI/Controls/Button/Button.xaml b/src/CrissCross.WPF.UI/Controls/Button/Button.xaml index 501a37a..af726f3 100644 --- a/src/CrissCross.WPF.UI/Controls/Button/Button.xaml +++ b/src/CrissCross.WPF.UI/Controls/Button/Button.xaml @@ -16,8 +16,8 @@ - - + + @@ -85,8 +85,8 @@ - - + + diff --git a/src/CrissCross.WPF.UI/Controls/MessageBox/MessageBox.cs b/src/CrissCross.WPF.UI/Controls/MessageBox/MessageBox.cs index 43f62b4..9a29ff4 100644 --- a/src/CrissCross.WPF.UI/Controls/MessageBox/MessageBox.cs +++ b/src/CrissCross.WPF.UI/Controls/MessageBox/MessageBox.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Drawing; +using System.Runtime.CompilerServices; using CrissCross.WPF.UI.Input; namespace CrissCross.WPF.UI.Controls; @@ -12,131 +13,102 @@ namespace CrissCross.WPF.UI.Controls; /// [ToolboxItem(true)] [ToolboxBitmap(typeof(MessageBox), "MessageBox.bmp")] -public class MessageBox : Window +public class MessageBox : System.Windows.Window { - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty ShowTitleProperty = DependencyProperty.Register( nameof(ShowTitle), typeof(bool), typeof(MessageBox), new PropertyMetadata(true)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty PrimaryButtonTextProperty = DependencyProperty.Register( nameof(PrimaryButtonText), typeof(string), typeof(MessageBox), new PropertyMetadata(string.Empty)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty SecondaryButtonTextProperty = DependencyProperty.Register( nameof(SecondaryButtonText), typeof(string), typeof(MessageBox), new PropertyMetadata(string.Empty)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty CloseButtonTextProperty = DependencyProperty.Register( nameof(CloseButtonText), typeof(string), typeof(MessageBox), new PropertyMetadata("Close")); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty PrimaryButtonIconProperty = DependencyProperty.Register( nameof(PrimaryButtonIcon), - typeof(SymbolRegular), + typeof(IconElement), typeof(MessageBox), - new PropertyMetadata(SymbolRegular.Empty)); + new PropertyMetadata(null)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty SecondaryButtonIconProperty = DependencyProperty.Register( nameof(SecondaryButtonIcon), - typeof(SymbolRegular), + typeof(IconElement), typeof(MessageBox), - new PropertyMetadata(SymbolRegular.Empty)); + new PropertyMetadata(null)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty CloseButtonIconProperty = DependencyProperty.Register( nameof(CloseButtonIcon), - typeof(SymbolRegular), + typeof(IconElement), typeof(MessageBox), - new PropertyMetadata(SymbolRegular.Empty)); + new PropertyMetadata(null)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty PrimaryButtonAppearanceProperty = DependencyProperty.Register( nameof(PrimaryButtonAppearance), typeof(ControlAppearance), typeof(MessageBox), new PropertyMetadata(ControlAppearance.Primary)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty SecondaryButtonAppearanceProperty = DependencyProperty.Register( nameof(SecondaryButtonAppearance), typeof(ControlAppearance), typeof(MessageBox), new PropertyMetadata(ControlAppearance.Secondary)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty CloseButtonAppearanceProperty = DependencyProperty.Register( nameof(CloseButtonAppearance), typeof(ControlAppearance), typeof(MessageBox), new PropertyMetadata(ControlAppearance.Secondary)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty IsPrimaryButtonEnabledProperty = DependencyProperty.Register( nameof(IsPrimaryButtonEnabled), typeof(bool), typeof(MessageBox), new PropertyMetadata(true)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty IsSecondaryButtonEnabledProperty = DependencyProperty.Register( nameof(IsSecondaryButtonEnabled), typeof(bool), typeof(MessageBox), new PropertyMetadata(true)); - /// - /// Property for . - /// + /// Identifies the dependency property. public static readonly DependencyProperty TemplateButtonCommandProperty = DependencyProperty.Register( nameof(TemplateButtonCommand), typeof(IRelayCommand), typeof(MessageBox), new PropertyMetadata(null)); - /// - /// The TCS. - /// -#pragma warning disable SA1401 // Fields should be private - protected TaskCompletionSource? Tcs; -#pragma warning restore SA1401 // Fields should be private + private static readonly PropertyInfo CanCenterOverWPFOwnerPropertyInfo = typeof(System.Windows.Window).GetProperty( + "CanCenterOverWPFOwner", + BindingFlags.NonPublic | BindingFlags.Instance)!; /// /// Initializes a new instance of the class. @@ -156,7 +128,7 @@ public MessageBox() } /// - /// Gets or sets a value indicating whether gets or sets a value that determines whether to show the Title in . + /// Gets or sets a value indicating whether to show the in . /// public bool ShowTitle { @@ -194,27 +166,27 @@ public string CloseButtonText /// /// Gets or sets the on the primary button. /// - public SymbolRegular PrimaryButtonIcon + public IconElement? PrimaryButtonIcon { - get => (SymbolRegular)GetValue(PrimaryButtonIconProperty); + get => (IconElement?)GetValue(PrimaryButtonIconProperty); set => SetValue(PrimaryButtonIconProperty, value); } /// /// Gets or sets the on the secondary button. /// - public SymbolRegular SecondaryButtonIcon + public IconElement? SecondaryButtonIcon { - get => (SymbolRegular)GetValue(SecondaryButtonIconProperty); + get => (IconElement?)GetValue(SecondaryButtonIconProperty); set => SetValue(SecondaryButtonIconProperty, value); } /// /// Gets or sets the on the close button. /// - public SymbolRegular CloseButtonIcon + public IconElement? CloseButtonIcon { - get => (SymbolRegular)GetValue(CloseButtonIconProperty); + get => (IconElement?)GetValue(CloseButtonIconProperty); set => SetValue(CloseButtonIconProperty, value); } @@ -246,7 +218,7 @@ public ControlAppearance CloseButtonAppearance } /// - /// Gets or sets a value indicating whether gets or sets whether the primary button is enabled. + /// Gets or sets a value indicating whether the primary button is enabled. /// public bool IsSecondaryButtonEnabled { @@ -255,7 +227,7 @@ public bool IsSecondaryButtonEnabled } /// - /// Gets or sets a value indicating whether gets or sets whether the secondary button is enabled. + /// Gets or sets a value indicating whether the secondary button is enabled. /// public bool IsPrimaryButtonEnabled { @@ -264,17 +236,24 @@ public bool IsPrimaryButtonEnabled } /// - /// Gets command triggered after clicking the button on the Footer. + /// Gets the command triggered after clicking the button on the Footer. /// public IRelayCommand TemplateButtonCommand => (IRelayCommand)GetValue(TemplateButtonCommandProperty); + /// + /// Gets or sets the TCS. + /// + /// + /// The TCS. + /// + protected TaskCompletionSource? Tcs { get; set; } + /// /// Shows this instance. /// /// $"Use {nameof(ShowDialogAsync)} instead. [Obsolete($"Use {nameof(ShowDialogAsync)} instead")] - public new void Show() => - throw new InvalidOperationException($"Use {nameof(ShowDialogAsync)} instead"); + public new void Show() => throw new InvalidOperationException($"Use {nameof(ShowDialogAsync)} instead"); /// /// Shows the dialog. @@ -282,16 +261,14 @@ public bool IsPrimaryButtonEnabled /// A bool. /// $"Use {nameof(ShowDialogAsync)} instead. [Obsolete($"Use {nameof(ShowDialogAsync)} instead")] - public new bool? ShowDialog() => - throw new InvalidOperationException($"Use {nameof(ShowDialogAsync)} instead"); + public new bool? ShowDialog() => throw new InvalidOperationException($"Use {nameof(ShowDialogAsync)} instead"); /// /// Closes this instance. /// /// $"Use {nameof(Close)} with MessageBoxResult instead. [Obsolete($"Use {nameof(Close)} with MessageBoxResult instead")] - public new void Close() => - throw new InvalidOperationException($"Use {nameof(Close)} with MessageBoxResult instead"); + public new void Close() => throw new InvalidOperationException($"Use {nameof(Close)} with MessageBoxResult instead"); /// /// Displays a message box. @@ -301,6 +278,7 @@ public bool IsPrimaryButtonEnabled /// /// . /// + /// Thrown if the operation is canceled. public async Task ShowDialogAsync( bool showAsDialog = true, CancellationToken cancellationToken = default) @@ -343,13 +321,35 @@ protected virtual void OnLoaded() var rootElement = (UIElement)GetVisualChild(0)!; ResizeToContentSize(rootElement); - CenterWindowOnScreen(); + + switch (WindowStartupLocation) + { + case WindowStartupLocation.Manual: + case WindowStartupLocation.CenterScreen: + CenterWindowOnScreen(); + break; + case WindowStartupLocation.CenterOwner: + if ( + !CanCenterOverWPFOwner() + || Owner.WindowState is WindowState.Maximized or WindowState.Minimized) + { + CenterWindowOnScreen(); + } + else + { + CenterWindowOnOwner(); + } + + break; + default: + throw new InvalidOperationException(); + } } /// - /// Sets Width and Height. + /// Resizes the MessageBox to fit the content's size, including margins. /// - /// The root element. + /// The root element of the MessageBox. protected virtual void ResizeToContentSize(UIElement rootElement) { if (rootElement == null) @@ -362,8 +362,8 @@ protected virtual void ResizeToContentSize(UIElement rootElement) // left and right margin const double margin = 12.0 * 2; - Width = desiredSize.Width + margin; - Height = desiredSize.Height; + SetCurrentValue(WidthProperty, desiredSize.Width + margin); + SetCurrentValue(HeightProperty, desiredSize.Height); ResizeWidth(rootElement); ResizeHeight(rootElement); @@ -372,7 +372,7 @@ protected virtual void ResizeToContentSize(UIElement rootElement) /// /// Raises the event. /// - /// The instance containing the event data. + /// The instance containing the event data. protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); @@ -382,7 +382,7 @@ protected override void OnClosing(CancelEventArgs e) return; } - Tcs?.TrySetResult(MessageBoxResult.None); + _ = Tcs?.TrySetResult(MessageBoxResult.None); } /// @@ -390,18 +390,17 @@ protected override void OnClosing(CancelEventArgs e) /// protected virtual void CenterWindowOnScreen() { - // TODO MessageBox should be displayed on the window on which the application var screenWidth = SystemParameters.PrimaryScreenWidth; var screenHeight = SystemParameters.PrimaryScreenHeight; - Left = (screenWidth / 2) - (Width / 2); - Top = (screenHeight / 2) - (Height / 2); + SetCurrentValue(LeftProperty, (screenWidth / 2) - (Width / 2)); + SetCurrentValue(TopProperty, (screenHeight / 2) - (Height / 2)); } /// - /// Occurs after the is clicked. + /// Occurs after the is clicked. /// - /// The button. + /// The MessageBox button. protected virtual void OnButtonClick(MessageBoxButton button) { var result = button switch @@ -411,14 +410,36 @@ protected virtual void OnButtonClick(MessageBoxButton button) _ => MessageBoxResult.None }; - Tcs?.TrySetResult(result); + _ = Tcs?.TrySetResult(result); base.Close(); } +#if NET8_0_OR_GREATER + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_CanCenterOverWPFOwner")] + private static extern bool CanCenterOverWPFOwnerAccessor(System.Windows.Window w); +#endif + + // CanCenterOverWPFOwner property see https://source.dot.net/#PresentationFramework/System/Windows/Window.cs,e679e433777b21b8 + private bool CanCenterOverWPFOwner() => +#if NET8_0_OR_GREATER + CanCenterOverWPFOwnerAccessor(this); +#else + (bool)CanCenterOverWPFOwnerPropertyInfo.GetValue(this)!; +#endif + + private void CenterWindowOnOwner() + { + var left = Owner.Left + ((Owner.Width - Width) / 2); + var top = Owner.Top + ((Owner.Height - Height) / 2); + + SetCurrentValue(LeftProperty, left); + SetCurrentValue(TopProperty, top); + } + private void RemoveTitleBarAndApplyMica() { - UnsafeNativeMethods.RemoveWindowTitlebarContents(this); - WindowBackdrop.ApplyBackdrop(this, WindowBackdropType.Mica); + _ = UnsafeNativeMethods.RemoveWindowTitlebarContents(this); + _ = WindowBackdrop.ApplyBackdrop(this, WindowBackdropType.Mica); } private void ResizeWidth(UIElement element) @@ -428,14 +449,14 @@ private void ResizeWidth(UIElement element) return; } - Width = MaxWidth; + SetCurrentValue(WidthProperty, MaxWidth); element.UpdateLayout(); - Height = element.DesiredSize.Height; + SetCurrentValue(HeightProperty, element.DesiredSize.Height); if (Height > MaxHeight) { - MaxHeight = Height; + SetCurrentValue(MaxHeightProperty, Height); } } @@ -446,14 +467,14 @@ private void ResizeHeight(UIElement element) return; } - Height = MaxHeight; + SetCurrentValue(HeightProperty, MaxHeight); element.UpdateLayout(); - Width = element.DesiredSize.Width; + SetCurrentValue(WidthProperty, element.DesiredSize.Width); if (Width > MaxWidth) { - MaxWidth = Width; + SetCurrentValue(MaxWidthProperty, Width); } } } diff --git a/src/CrissCross.WPF.UI/Controls/PasswordBox/PasswordBox.cs b/src/CrissCross.WPF.UI/Controls/PasswordBox/PasswordBox.cs index 8d988f4..f405315 100644 --- a/src/CrissCross.WPF.UI/Controls/PasswordBox/PasswordBox.cs +++ b/src/CrissCross.WPF.UI/Controls/PasswordBox/PasswordBox.cs @@ -2,6 +2,7 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Windows.Controls; namespace CrissCross.WPF.UI.Controls; @@ -56,12 +57,17 @@ public class PasswordBox : TextBox typeof(RoutedEventHandler), typeof(PasswordBox)); + private readonly PasswordHelper _passwordHelper; private bool _lockUpdatingContents; /// /// Initializes a new instance of the class. /// - public PasswordBox() => _lockUpdatingContents = false; + public PasswordBox() + { + _lockUpdatingContents = false; + _passwordHelper = new(this); + } /// /// Event fired from this text box when its inner content @@ -266,54 +272,118 @@ private void UpdateTextContents(bool isTriggeredByTextInput) } var caretIndex = CaretIndex; - var selectionIndex = SelectionStart; - var currentPassword = Password ?? string.Empty; - var newPasswordValue = currentPassword; + var newPasswordValue = _passwordHelper.GetPassword(); if (isTriggeredByTextInput) { - var currentText = Text; - var newCharacters = currentText.Replace(PasswordChar.ToString(), string.Empty); + newPasswordValue = _passwordHelper.GetNewPassword(); + } + + _lockUpdatingContents = true; - if (currentText.Length < currentPassword.Length) + Text = new string(PasswordChar, newPasswordValue?.Length ?? 0); + Password = newPasswordValue ?? string.Empty; + CaretIndex = caretIndex; + + RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); + + _lockUpdatingContents = false; + } + + private class PasswordHelper(PasswordBox passwordBox) + { + private string _currentText = string.Empty; + private string _newPasswordValue = string.Empty; + private string _currentPassword = string.Empty; + + public string GetNewPassword() + { + _currentPassword = GetPassword(); + _newPasswordValue = _currentPassword; + _currentText = passwordBox.Text; + var selectionIndex = passwordBox.SelectionStart; + var passwordChar = passwordBox.PasswordChar; + var newCharacters = _currentText.Replace(passwordChar.ToString(), string.Empty); + var isDeleted = false; + + if (IsDeleteOption()) { - newPasswordValue = currentPassword.Remove(selectionIndex, currentPassword.Length - currentText.Length); + _newPasswordValue = _currentPassword.Remove( + selectionIndex, + _currentPassword.Length - _currentText.Length); + isDeleted = true; } - if (newCharacters.Length > 1) + switch (newCharacters.Length) { - var index = currentText.IndexOf(newCharacters[0]); + case > 1: + { + var index = _currentText.IndexOf(newCharacters[0]); - newPasswordValue = - index > newPasswordValue.Length - 1 - ? newPasswordValue + newCharacters - : newPasswordValue.Insert(index, newCharacters); - } - else - { - for (var i = 0; i < currentText.Length; i++) - { - if (currentText[i] == PasswordChar) + _newPasswordValue = + index > _newPasswordValue.Length - 1 + ? _newPasswordValue + newCharacters + : _newPasswordValue.Insert(index, newCharacters); + break; + } + + case 1: { - continue; + for (var i = 0; i < _currentText.Length; i++) + { + if (_currentText[i] == passwordChar) + { + continue; + } + + UpdatePasswordWithInputCharacter(i, _currentText[i].ToString()); + break; + } + + break; } - newPasswordValue = - currentText.Length == newPasswordValue.Length - ? newPasswordValue.Remove(i, 1).Insert(i, currentText[i].ToString()) - : newPasswordValue.Insert(i, currentText[i].ToString()); - } + case 0 when !isDeleted: + { + // The input is a PasswordChar, which is to be inserted at the designated position. + var insertIndex = selectionIndex - 1; + UpdatePasswordWithInputCharacter(insertIndex, passwordChar.ToString()); + break; + } } + + return _newPasswordValue; } - _lockUpdatingContents = true; + public string GetPassword() => passwordBox.Password ?? string.Empty; - Text = new string(PasswordChar, newPasswordValue?.Length ?? 0); - Password = newPasswordValue ?? string.Empty; - CaretIndex = caretIndex; + private void UpdatePasswordWithInputCharacter(int insertIndex, string insertValue) + { + Debug.Assert( + _currentText == passwordBox.Text, + "_currentText == _passwordBox.Text"); - RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); + if (_currentText.Length == _newPasswordValue.Length) + { + // If it's a direct character replacement, remove the existing one before inserting the new one. + _newPasswordValue = _newPasswordValue.Remove(insertIndex, 1).Insert(insertIndex, insertValue); + } + else + { + _newPasswordValue = _newPasswordValue.Insert(insertIndex, insertValue); + } + } - _lockUpdatingContents = false; + private bool IsDeleteOption() + { + Debug.Assert( + _currentText == passwordBox.Text, + "_currentText == _passwordBox.Text"); + Debug.Assert( + _currentPassword == passwordBox.Password, + "_currentPassword == _passwordBox.Password"); + + return _currentText.Length < _currentPassword.Length; + } } } diff --git a/src/CrissCross.WPF.UI/Controls/TreeView/TreeView.xaml b/src/CrissCross.WPF.UI/Controls/TreeView/TreeView.xaml index 606f661..7cef7e2 100644 --- a/src/CrissCross.WPF.UI/Controls/TreeView/TreeView.xaml +++ b/src/CrissCross.WPF.UI/Controls/TreeView/TreeView.xaml @@ -2,7 +2,6 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:CrissCross.WPF.UI.Controls"> - + +