From 2559c5e833d6872970cf7791e1cf0e8d35c153cd Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 11 Sep 2024 12:19:57 -0700 Subject: [PATCH] Added TitleBar control --- README.md | 1 + docs/index.md | 1 + src/Dependencies.targets | 4 +- src/Directory.Build.targets | 4 +- src/WinUIEx/Themes/Generic.xaml | 9 + src/WinUIEx/TitleBar/TitleBar.cs | 782 ++++++++++++++++++ src/WinUIEx/TitleBar/TitleBar.xaml | 285 +++++++ .../TitleBar/TitleBarAutomationPeer.cs | 43 + .../TitleBar/TitleBarTemplateSettings.cs | 30 + .../TitleBar/TitleBar_themeresources.xaml | 192 +++++ src/WinUIEx/WinUIEx.csproj | 2 +- src/WinUIExSample/MainWindow.xaml | 26 +- src/WinUIExSample/MainWindow.xaml.cs | 21 +- 13 files changed, 1380 insertions(+), 20 deletions(-) create mode 100644 src/WinUIEx/Themes/Generic.xaml create mode 100644 src/WinUIEx/TitleBar/TitleBar.cs create mode 100644 src/WinUIEx/TitleBar/TitleBar.xaml create mode 100644 src/WinUIEx/TitleBar/TitleBarAutomationPeer.cs create mode 100644 src/WinUIEx/TitleBar/TitleBarTemplateSettings.cs create mode 100644 src/WinUIEx/TitleBar/TitleBar_themeresources.xaml diff --git a/README.md b/README.md index a498f4b..e850406 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Watch WinUIEx covered in the On .NET Live show: - [Splash Screens](https://dotmorten.github.io/WinUIEx/concepts/Splashscreen.html) - [OAuth Web Authentication](https://dotmorten.github.io/WinUIEx/concepts/WebAuthenticator.html) - [Custom Window Backdrops](https://dotmorten.github.io/WinUIEx/concepts/CustomBackdrops.html) + - TitleBar control - Code analyzers for Windows App SDK APIs to guide the developer. diff --git a/docs/index.md b/docs/index.md index a410309..7e1a035 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,7 @@ A set of extension methods and classes to fill some gaps in WinUI 3, mostly arou - [Splash screen](concepts/Splashscreen.md) - [OAuth Web Authenticator](concepts/WebAuthenticator.md) - [Custom Backdrops](concepts/CustomBackdrops.md) + - TitleBar And more to come... diff --git a/src/Dependencies.targets b/src/Dependencies.targets index f64151b..5d88546 100644 --- a/src/Dependencies.targets +++ b/src/Dependencies.targets @@ -1,8 +1,8 @@ - 1.4.230822000 - 10.0.22621.1 + 1.5.240227000 + 10.0.22621.756 diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index b369503..44a0e10 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,8 +17,8 @@ Morten Nielsen - https://xaml.dev Morten Nielsen - https://xaml.dev logo.png - 2.3.4 - 2.3.3 + 2.4.0 + 2.3.4 diff --git a/src/WinUIEx/Themes/Generic.xaml b/src/WinUIEx/Themes/Generic.xaml new file mode 100644 index 0000000..0fb1906 --- /dev/null +++ b/src/WinUIEx/Themes/Generic.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/WinUIEx/TitleBar/TitleBar.cs b/src/WinUIEx/TitleBar/TitleBar.cs new file mode 100644 index 0000000..4355153 --- /dev/null +++ b/src/WinUIEx/TitleBar/TitleBar.cs @@ -0,0 +1,782 @@ +using System.Collections.Generic; +using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Peers; +using Microsoft.UI.Xaml.Controls; + +namespace WinUIEx; + +/// +/// TitleBar control. +/// +public class TitleBar : Control +{ + private double m_compactModeThresholdWidth = 0.0; + const string s_leftPaddingColumnName = "LeftPaddingColumn"; + const string s_rightPaddingColumnName = "RightPaddingColumn"; + const string s_layoutRootPartName = "PART_LayoutRoot"; + const string s_backButtonPartName = "PART_BackButton"; + const string s_paneToggleButtonPartName = "PART_PaneToggleButton"; + const string s_headerContentPresenterPartName = "PART_HeaderContentPresenter"; + const string s_contentPresenterGridPartName = "PART_ContentPresenterGrid"; + const string s_contentPresenterPartName = "PART_ContentPresenter"; + const string s_footerPresenterPartName = "PART_FooterContentPresenter"; + const string s_compactVisualStateName = "Compact"; + const string s_expandedVisualStateName = "Expanded"; + const string s_compactHeightVisualStateName = "CompactHeight"; + const string s_expandedHeightVisualStateName = "ExpandedHeight"; + const string s_defaultSpacingVisualStateName = "DefaultSpacing"; + const string s_negativeInsetVisualStateName = "NegativeInsetSpacing"; + const string s_iconVisibleVisualStateName = "IconVisible"; + const string s_iconCollapsedVisualStateName = "IconCollapsed"; + const string s_iconDeactivatedVisualStateName = "IconDeactivated"; + const string s_backButtonVisibleVisualStateName = "BackButtonVisible"; + const string s_backButtonCollapsedVisualStateName = "BackButtonCollapsed"; + const string s_backButtonDeactivatedVisualStateName = "BackButtonDeactivated"; + const string s_paneToggleButtonVisibleVisualStateName = "PaneToggleButtonVisible"; + const string s_paneToggleButtonCollapsedVisualStateName = "PaneToggleButtonCollapsed"; + const string s_paneToggleButtonDeactivatedVisualStateName = "PaneToggleButtonDeactivated"; + const string s_titleTextVisibleVisualStateName = "TitleTextVisible"; + const string s_titleTextCollapsedVisualStateName = "TitleTextCollapsed"; + const string s_titleTextDeactivatedVisualStateName = "TitleTextDeactivated"; + const string s_subtitleTextVisibleVisualStateName = "SubtitleTextVisible"; + const string s_subtitleTextCollapsedVisualStateName = "SubtitleTextCollapsed"; + const string s_subtitleTextDeactivatedVisualStateName = "SubtitleTextDeactivated"; + const string s_headerVisibleVisualStateName = "HeaderVisible"; + const string s_headerCollapsedVisualStateName = "HeaderCollapsed"; + const string s_headerDeactivatedVisualStateName = "HeaderDeactivated"; + const string s_contentVisibleVisualStateName = "ContentVisible"; + const string s_contentCollapsedVisualStateName = "ContentCollapsed"; + const string s_contentDeactivatedVisualStateName = "ContentDeactivated"; + const string s_footerVisibleVisualStateName = "FooterVisible"; + const string s_footerCollapsedVisualStateName = "FooterCollapsed"; + const string s_footerDeactivatedVisualStateName = "FooterDeactivated"; + const string s_titleBarButtonForegroundColorName = "TitleBarButtonForegroundColor"; + const string s_titleBarButtonBackgroundColorName = "TitleBarButtonBackgroundColor"; + const string s_titleBarButtonHoverForegroundColorName = "TitleBarButtonHoverForegroundColor"; + const string s_titleBarButtonHoverBackgroundColorName = "TitleBarButtonHoverBackgroundColor"; + const string s_titleBarButtonPressedForegroundColorName = "TitleBarButtonPressedForegroundColor"; + const string s_titleBarButtonPressedBackgroundColorName = "TitleBarButtonPressedBackgroundColor"; + const string s_titleBarButtonInactiveForegroundColorName = "TitleBarButtonInactiveForegroundColor"; + + private ColumnDefinition? m_leftPaddingColumn; + private ColumnDefinition? m_rightPaddingColumn; + private Button? m_backButton; + private Button? m_paneToggleButton; + private Grid? m_contentAreaGrid; + private FrameworkElement? m_headerArea; + private FrameworkElement? m_contentArea; + private FrameworkElement? m_footerArea; + private InputActivationListener? m_inputActivationListener; + + /// + /// Initializes a new instance of the class. + /// + public TitleBar() + { + SetValue(TemplateSettingsProperty, new TitleBarTemplateSettings()); + this.DefaultStyleKey = typeof(TitleBar); + this.SizeChanged += OnSizeChanged; + this.LayoutUpdated += OnLayoutUpdated; + ActualThemeChanged += (s, e) => UpdateTheme(); + } + + /// + /// Finalizer + /// + ~TitleBar() + { + if (m_inputActivationListener != null) + m_inputActivationListener.InputActivationChanged -= OnInputActivationChanged; + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + m_leftPaddingColumn = GetTemplateChild(s_leftPaddingColumnName) as ColumnDefinition; + m_rightPaddingColumn = GetTemplateChild(s_rightPaddingColumnName) as ColumnDefinition; + if (XamlRoot?.ContentIslandEnvironment is not null) + { + var appWindowId = XamlRoot.ContentIslandEnvironment.AppWindowId; + if (appWindowId.Value != 0) + { + m_inputActivationListener = Microsoft.UI.Input.InputActivationListener.GetForWindowId(appWindowId); + m_inputActivationListener.InputActivationChanged += OnInputActivationChanged; + } + } + UpdateHeight(); + UpdatePadding(); + UpdateIcon(); + UpdateBackButton(); + UpdatePaneToggleButton(); + UpdateTheme(); + UpdateTitle(); + UpdateSubtitle(); + UpdateHeader(); + UpdateContent(); + UpdateFooter(); + UpdateInteractableElementsList(); + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if(Content is not null) + { + if(m_contentArea is not null && m_contentAreaGrid is not null) + { + if(m_compactModeThresholdWidth != 0 && m_contentArea.DesiredSize.Width > m_contentAreaGrid.ActualWidth) + { + m_compactModeThresholdWidth = e.NewSize.Width; + } + else if(e.NewSize.Width >= m_compactModeThresholdWidth) + { + m_compactModeThresholdWidth = 0; + VisualStateManager.GoToState(this, s_expandedVisualStateName, false); + UpdateTitle(); + UpdateSubtitle(); + } + } + } + UpdateDragRegion(); + } + + private void OnLayoutUpdated(object? sender, object e) + { + UpdateDragRegion(); + } + + private void OnInputActivationChanged(InputActivationListener sender, InputActivationListenerActivationChangedEventArgs args) + { + bool isDeactivate = sender.State == InputActivationState.Deactivated; + if (IsBackButtonVisible && IsBackEnabled) + { + VisualStateManager.GoToState(this, isDeactivate ? s_backButtonDeactivatedVisualStateName : s_backButtonVisibleVisualStateName, false); + } + + if (IsPaneToggleButtonVisible) + { + VisualStateManager.GoToState(this, isDeactivate ? s_paneToggleButtonDeactivatedVisualStateName : s_paneToggleButtonVisibleVisualStateName, false); + } + + if (IconSource is not null) + { + VisualStateManager.GoToState(this, isDeactivate ? s_iconDeactivatedVisualStateName : s_iconVisibleVisualStateName, false); + } + + if (!string.IsNullOrEmpty(Title)) + { + VisualStateManager.GoToState(this, isDeactivate ? s_titleTextDeactivatedVisualStateName : s_titleTextVisibleVisualStateName, false); + } + + if (!string.IsNullOrEmpty(Subtitle)) + { + VisualStateManager.GoToState(this, isDeactivate ? s_subtitleTextDeactivatedVisualStateName : s_subtitleTextVisibleVisualStateName, false); + } + + if (Header is not null) + { + VisualStateManager.GoToState(this, isDeactivate ? s_headerDeactivatedVisualStateName : s_headerVisibleVisualStateName, false); + } + + if (Content is not null) + { + VisualStateManager.GoToState(this, isDeactivate ? s_contentDeactivatedVisualStateName : s_contentVisibleVisualStateName, false); + } + + if (Footer is not null) + { + VisualStateManager.GoToState(this, isDeactivate ? s_footerDeactivatedVisualStateName : s_footerVisibleVisualStateName, false); + } + } + + private void OnBackButtonClick(object sender, RoutedEventArgs e) + { + BackRequested?.Invoke(this, e); + } + + private void OnPaneToggleButtonClick(object sender, RoutedEventArgs e) + { + PaneToggleRequested?.Invoke(this, e); + } + + private void UpdateIcon() + { + if ((IconSource is not null)) + { + TemplateSettings.IconElement = MakeIconElementFrom(IconSource); + VisualStateManager.GoToState(this, s_iconVisibleVisualStateName, false); + } + else + { + TemplateSettings.IconElement = null; + VisualStateManager.GoToState(this, s_iconCollapsedVisualStateName, false); + } + } + + private IconElement? MakeIconElementFrom(IconSource iconSource) + { + if (iconSource is FontIconSource fontIconSource) + { + FontIcon fontIcon = new FontIcon(); + + fontIcon.Glyph = fontIconSource.Glyph; + fontIcon.FontSize = fontIconSource.FontSize; + if (fontIconSource.Foreground != null) + { + fontIcon.Foreground = fontIconSource.Foreground; + } + + if (fontIconSource.FontFamily != null) + { + fontIcon.FontFamily = fontIconSource.FontFamily; + } + + fontIcon.FontWeight = fontIconSource.FontWeight; + fontIcon.FontStyle = fontIconSource.FontStyle; + fontIcon.IsTextScaleFactorEnabled = fontIconSource.IsTextScaleFactorEnabled; + fontIcon.MirroredWhenRightToLeft = fontIconSource.MirroredWhenRightToLeft; + + return fontIcon; + } + else if (iconSource is SymbolIconSource symbolIconSource) + { + SymbolIcon symbolIcon = new SymbolIcon(); + symbolIcon.Symbol = symbolIconSource.Symbol; + if (symbolIconSource.Foreground != null) + { + symbolIcon.Foreground = symbolIconSource.Foreground; + } + return symbolIcon; + } + // Note: this check must be done before BitmapIconSource + // since ImageIconSource uses BitmapIconSource as a composable interface, + // so a ImageIconSource will also register as a BitmapIconSource. + else if (iconSource is ImageIconSource imageIconSource) + { + ImageIcon imageIcon = new ImageIcon(); + if (imageIconSource.ImageSource != null) + { + imageIcon.Source = imageIconSource.ImageSource; + } + if (imageIconSource.Foreground != null) + { + imageIcon.Foreground = imageIconSource.Foreground; + } + return imageIcon; + } + else if (iconSource is BitmapIconSource bitmapIconSource) + { + BitmapIcon bitmapIcon = new BitmapIcon(); + + if (bitmapIconSource.UriSource != null) + { + bitmapIcon.UriSource = bitmapIconSource.UriSource; + } + + bitmapIcon.ShowAsMonochrome = bitmapIconSource.ShowAsMonochrome; + + if (bitmapIconSource.Foreground != null) + { + bitmapIcon.Foreground = bitmapIconSource.Foreground; + } + + return bitmapIcon; + } + // Note: this check must be done before PathIconSource + // since AnimatedIconSource uses PathIconSource as a composable interface, + // so a AnimatedIconSource will also register as a PathIconSource. + else if (iconSource is AnimatedIconSource animatedIconSource) + { + AnimatedIcon animatedIcon = new AnimatedIcon(); + if (animatedIconSource.Source != null) + { + animatedIcon.Source = animatedIconSource.Source; + } + if (animatedIconSource.FallbackIconSource != null) + { + animatedIcon.FallbackIconSource = animatedIconSource.FallbackIconSource; + } + if (animatedIconSource.Foreground != null) + { + animatedIcon.Foreground = animatedIconSource.Foreground; + } + return animatedIcon; + } + else if (iconSource is PathIconSource pathIconSource) + { + PathIcon pathIcon = new PathIcon(); + + if (pathIconSource.Data != null) + { + pathIcon.Data = pathIconSource.Data; + } + if (pathIconSource.Foreground != null) + { + pathIcon.Foreground = pathIconSource.Foreground; + } + return pathIcon; + } + + return null; + } + + private void UpdateBackButton() + { + if (IsBackButtonVisible) + { + if (m_backButton is null) + { + LoadBackButton(); + } + + VisualStateManager.GoToState(this, s_backButtonVisibleVisualStateName, false); + } + else + { + VisualStateManager.GoToState(this, s_backButtonCollapsedVisualStateName, false); + } + + UpdateInteractableElementsList(); + UpdateHeaderSpacing(); + } + + private void UpdatePaneToggleButton() + { + if (IsPaneToggleButtonVisible) + { + if (m_paneToggleButton is null) + { + LoadPaneToggleButton(); + } + + VisualStateManager.GoToState(this, s_paneToggleButtonVisibleVisualStateName, false); + } + else + { + VisualStateManager.GoToState(this, s_paneToggleButtonCollapsedVisualStateName, false); + } + + UpdateInteractableElementsList(); + UpdateHeaderSpacing(); + } + + private void UpdateHeight() + { + VisualStateManager.GoToState(this, (Content is null && Header is null && Footer is null) ? s_compactHeightVisualStateName : s_expandedHeightVisualStateName, false); + } + + private void UpdatePadding() + { + if (XamlRoot?.ContentIslandEnvironment is not null) + { + var appWindowId = XamlRoot.ContentIslandEnvironment.AppWindowId; + var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(appWindowId); + + if (appWindow.TitleBar is not null) + { + // TODO 50724421: Bind to appTitleBar Left and Right inset changed event. + if (m_leftPaddingColumn is not null) + { + m_leftPaddingColumn.Width = new GridLength(appWindow.TitleBar.LeftInset); + } + + if (m_rightPaddingColumn is not null) + { + m_rightPaddingColumn.Width = new GridLength(appWindow.TitleBar.RightInset); + } + } + } + } + + private object? ResourceLookup(string key) + { + return this.Resources.ContainsKey(key) ? this.Resources[key] : Application.Current.Resources.ContainsKey(key) ? Application.Current.Resources[key] : null; + } + + private void UpdateTheme() + { + if (XamlRoot?.ContentIslandEnvironment is not null) + { + var appWindowId = XamlRoot.ContentIslandEnvironment.AppWindowId; + var appTitleBar = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(appWindowId)?.TitleBar; + // AppWindow TitleBar's caption buttons does not update colors with theme change. + // We need to set them here. + if (appTitleBar is not null) + { + // Rest colors. + var buttonForegroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonForegroundColorName); + appTitleBar.ButtonForegroundColor = buttonForegroundColor; + + var buttonBackgroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonBackgroundColorName); + appTitleBar.ButtonBackgroundColor = buttonBackgroundColor; + appTitleBar.ButtonInactiveBackgroundColor = buttonBackgroundColor; + + // Hover colors. + var buttonHoverForegroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonHoverForegroundColorName); + appTitleBar.ButtonHoverForegroundColor = buttonHoverForegroundColor; + + var buttonHoverBackgroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonHoverBackgroundColorName); + appTitleBar.ButtonHoverBackgroundColor = buttonHoverBackgroundColor; + + // Pressed colors. + var buttonPressedForegroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonPressedForegroundColorName); + appTitleBar.ButtonPressedForegroundColor = buttonPressedForegroundColor; + + var buttonPressedBackgroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonPressedBackgroundColorName); + appTitleBar.ButtonPressedBackgroundColor= buttonPressedBackgroundColor; + + // Inactive foreground. + var buttonInactiveForegroundColor = (Windows.UI.Color?)ResourceLookup(s_titleBarButtonInactiveForegroundColorName); + appTitleBar.ButtonInactiveForegroundColor =buttonInactiveForegroundColor; + } + } + } + + private void UpdateTitle() + { + if (string.IsNullOrEmpty(Title)) + { + VisualStateManager.GoToState(this, s_titleTextCollapsedVisualStateName, false); + } + else + { + VisualStateManager.GoToState(this, s_titleTextVisibleVisualStateName, false); + } + } + + private void UpdateSubtitle() + { + if (string.IsNullOrEmpty(Subtitle)) + { + VisualStateManager.GoToState(this, s_subtitleTextCollapsedVisualStateName, false); + } + else + { + VisualStateManager.GoToState(this, s_subtitleTextVisibleVisualStateName, false); + } + } + + private void UpdateHeader() + { + if (Header is null) + { + VisualStateManager.GoToState(this, s_headerCollapsedVisualStateName, false); + } + else + { + if (m_headerArea is null) + { + m_headerArea = GetTemplateChild(s_headerContentPresenterPartName) as FrameworkElement; + } + VisualStateManager.GoToState(this, s_headerVisibleVisualStateName, false); + } + + UpdateHeight(); + UpdateInteractableElementsList(); + } + private void UpdateContent() + { + if (Content is null) + { + VisualStateManager.GoToState(this, s_contentCollapsedVisualStateName, false); + } + else + { + if (m_contentArea is null) + { + m_contentAreaGrid = GetTemplateChild(s_contentPresenterGridPartName) as Grid; + m_contentArea = GetTemplateChild(s_contentPresenterPartName) as FrameworkElement; + } + + VisualStateManager.GoToState(this, s_contentVisibleVisualStateName, false); + } + + UpdateHeight(); + UpdateInteractableElementsList(); + } + private void UpdateFooter() + { + if (Footer is null) + { + VisualStateManager.GoToState(this, s_footerCollapsedVisualStateName, false); + } + else + { + if (m_footerArea is null) + { + m_footerArea = GetTemplateChild(s_footerPresenterPartName) as FrameworkElement; + } + VisualStateManager.GoToState(this, s_footerVisibleVisualStateName, false); + } + + UpdateHeight(); + UpdateInteractableElementsList(); + } + + private readonly List m_interactableElementsList = new List(); + + private void UpdateDragRegion() + { + if (XamlRoot?.ContentIslandEnvironment is not null) + { + var appWindowId = XamlRoot.ContentIslandEnvironment.AppWindowId; + var nonClientPointerSource = InputNonClientPointerSource.GetForWindowId(appWindowId); + + if (m_interactableElementsList.Count > 0) + { + List passthroughRects = new List(); + + // Get rects for each interactable element in TitleBar. + foreach (var frameworkElement in m_interactableElementsList) + { + var transformBounds = frameworkElement.TransformToVisual(null); + var width = frameworkElement.ActualWidth; + var height = frameworkElement.ActualHeight; + var bounds = transformBounds.TransformBounds(new Windows.Foundation.Rect(0.0f, 0.0f, width, height)); + + if (bounds.X < 0 || bounds.Y < 0) + { + continue; + } + + var scale = XamlRoot.RasterizationScale; + var transparentRect = new Windows.Graphics.RectInt32( + (int)(bounds.X * scale), + (int)(bounds.Y * scale), + (int)(bounds.Width * scale), + (int)(bounds.Height * scale)); + + passthroughRects.Add(transparentRect); + } + + // Set list of rects as passthrough regions for the non-client area. + nonClientPointerSource.SetRegionRects(NonClientRegionKind.Passthrough, [.. passthroughRects]); + } + else + { + // There is no interactable areas. Clear previous passthrough rects. + nonClientPointerSource.ClearRegionRects(NonClientRegionKind.Passthrough); + } + } + } + + private void UpdateInteractableElementsList() + { + m_interactableElementsList.Clear(); + + if (IsBackButtonVisible && IsBackEnabled && m_backButton is not null) + { + m_interactableElementsList.Add(m_backButton); + } + + if (IsPaneToggleButtonVisible && m_paneToggleButton is not null) + { + m_interactableElementsList.Add(m_paneToggleButton); + } + + if (Header is not null && m_headerArea is not null) + { + m_interactableElementsList.Add(m_headerArea); + } + + if (Content is not null && m_contentArea is not null) + { + m_interactableElementsList.Add(m_contentArea); + } + + + if (Footer is not null && m_footerArea is not null) + { + m_interactableElementsList.Add(m_footerArea); + } + } + private void UpdateHeaderSpacing() + { + VisualStateManager.GoToState(this, IsBackButtonVisible == IsPaneToggleButtonVisible ? s_defaultSpacingVisualStateName : s_negativeInsetVisualStateName, false); + } + + private void LoadBackButton() + { + m_backButton = GetTemplateChild(s_backButtonPartName) as Button; + + if (m_backButton is not null) + { + m_backButton.Click += OnBackButtonClick; + // Do localization for the back button + if (string.IsNullOrEmpty(AutomationProperties.GetName(m_backButton))) + { + AutomationProperties.SetName(m_backButton, "Back"); + } + + // Setup the tooltip for the back button + var tooltip = new ToolTip(); + tooltip.Content = "Back"; + ToolTipService.SetToolTip(m_backButton, tooltip); + } + } + + private void LoadPaneToggleButton() + { + m_paneToggleButton = GetTemplateChild(s_paneToggleButtonPartName) as Button; + + if (m_paneToggleButton is not null) + { + m_paneToggleButton.Click += OnPaneToggleButtonClick; + + // Do localization for paneToggleButton + if (string.IsNullOrEmpty(AutomationProperties.GetName(m_paneToggleButton))) + { + AutomationProperties.SetName(m_paneToggleButton, "Toggle Navigation"); + } + + // Setup the tooltip for the paneToggleButton + var tooltip = new ToolTip(); + tooltip.Content = AutomationProperties.GetName(m_paneToggleButton); + ToolTipService.SetToolTip(m_paneToggleButton, tooltip); + } + } + + /// + protected override AutomationPeer OnCreateAutomationPeer() => new TitleBarAutomationPeer(this); + + /// + /// Gets or sets the Icon for the titlebar + /// + public IconSource IconSource + { + get { return (IconSource)GetValue(IconSourceProperty); } + set { SetValue(IconSourceProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty IconSourceProperty = + DependencyProperty.Register("IconSource", typeof(IconSource), typeof(TitleBar), new PropertyMetadata(null, (s, e) => ((TitleBar)s).UpdateIcon())); + + /// + /// Gets or sets the Header content for the titlebar + /// + public object? Header + { + get { return (object)GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty HeaderProperty = + DependencyProperty.Register("Header", typeof(object), typeof(TitleBar), new PropertyMetadata(null, (s, e) => ((TitleBar)s).UpdateHeader())); + + /// + /// Gets or sets the Window title + /// + public string Title + { + get { return (string)GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register("Title", typeof(string), typeof(TitleBar), new PropertyMetadata(string.Empty, (s, e) => ((TitleBar)s).UpdateTitle())); + + /// + /// Gets or sets the Subtitle for the titlebar + /// + public string Subtitle + { + get { return (string)GetValue(SubtitleProperty); } + set { SetValue(SubtitleProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SubtitleProperty = + DependencyProperty.Register("Subtitle", typeof(string), typeof(TitleBar), new PropertyMetadata(string.Empty, (s, e) => ((TitleBar)s).UpdateSubtitle())); + + /// + /// Gets or sets the content for the titlebar + /// + public object? Content + { + get { return (object?)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty ContentProperty = + DependencyProperty.Register("Content", typeof(object), typeof(TitleBar), new PropertyMetadata(null, (s, e) => ((TitleBar)s).UpdateContent())); + + /// + /// Gets or sets the footer of the titlebar + /// + public object? Footer + { + get { return (object?)GetValue(FooterProperty); } + set { SetValue(FooterProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty FooterProperty = + DependencyProperty.Register("Footer", typeof(object), typeof(TitleBar), new PropertyMetadata(null, (s, e) => ((TitleBar)s).UpdateFooter())); + + /// + /// Gets or sets a value indicating whether the back button is visible + /// + public bool IsBackButtonVisible + { + get { return (bool)GetValue(IsBackButtonVisibleProperty); } + set { SetValue(IsBackButtonVisibleProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty IsBackButtonVisibleProperty = + DependencyProperty.Register("IsBackButtonVisible", typeof(bool), typeof(TitleBar), new PropertyMetadata(false, (s,e) => ((TitleBar)s).UpdateBackButton())); + + /// + /// Gets or sets a value indicating whether the back button is enabled + /// + public bool IsBackEnabled + { + get { return (bool)GetValue(IsBackEnabledProperty); } + set { SetValue(IsBackEnabledProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty IsBackEnabledProperty = + DependencyProperty.Register("IsBackEnabled", typeof(bool), typeof(TitleBar), new PropertyMetadata(true, (s, e) => ((TitleBar)s).UpdateInteractableElementsList())); + + /// + /// Gets or sets a value indicating whether the pane toggle button is visible + /// + public bool IsPaneToggleButtonVisible + { + get { return (bool)GetValue(IsPaneToggleButtonVisibleProperty); } + set { SetValue(IsPaneToggleButtonVisibleProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty IsPaneToggleButtonVisibleProperty = + DependencyProperty.Register("IsPaneToggleButtonVisible", typeof(bool), typeof(TitleBar), new PropertyMetadata(false, (s, e) => ((TitleBar)s).UpdatePaneToggleButton())); + + /// + /// Gets the template settings for the titlebar + /// + public TitleBarTemplateSettings TemplateSettings + { + get { return (TitleBarTemplateSettings)GetValue(TemplateSettingsProperty); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty TemplateSettingsProperty = + DependencyProperty.Register("TemplateSettings", typeof(TitleBarTemplateSettings), typeof(TitleBar), new PropertyMetadata(null)); + + /// + /// Raised when the back button is clicked + /// + public event Windows.Foundation.TypedEventHandler? BackRequested; + + /// + /// Raised when the Pane toggle button is clicked + /// + public event Windows.Foundation.TypedEventHandler? PaneToggleRequested; +} diff --git a/src/WinUIEx/TitleBar/TitleBar.xaml b/src/WinUIEx/TitleBar/TitleBar.xaml new file mode 100644 index 0000000..0b8cb78 --- /dev/null +++ b/src/WinUIEx/TitleBar/TitleBar.xaml @@ -0,0 +1,285 @@ + + + + + + + + + diff --git a/src/WinUIEx/TitleBar/TitleBarAutomationPeer.cs b/src/WinUIEx/TitleBar/TitleBarAutomationPeer.cs new file mode 100644 index 0000000..c3721c2 --- /dev/null +++ b/src/WinUIEx/TitleBar/TitleBarAutomationPeer.cs @@ -0,0 +1,43 @@ +using Microsoft.UI.Xaml.Automation.Peers; + +namespace WinUIEx; + +/// +/// Automation peer for the control. +/// +public class TitleBarAutomationPeer : FrameworkElementAutomationPeer +{ + /// + /// Initializes a new instance of the class. + /// + /// TitleBar owner + public TitleBarAutomationPeer(TitleBar owner) : base(owner) + { + } + + /// + protected override AutomationControlType GetAutomationControlTypeCore() + { + return base.GetAutomationControlTypeCore(); + } + + /// + protected override string GetClassNameCore() + { + return nameof(TitleBar); + } + + /// + protected override string GetNameCore() + { + var name = base.GetNameCore(); + if (string.IsNullOrEmpty(name)) + { + if (Owner is TitleBar titleBar) + { + name = titleBar.Name; + } + } + return name; + } +} diff --git a/src/WinUIEx/TitleBar/TitleBarTemplateSettings.cs b/src/WinUIEx/TitleBar/TitleBarTemplateSettings.cs new file mode 100644 index 0000000..8138cbf --- /dev/null +++ b/src/WinUIEx/TitleBar/TitleBarTemplateSettings.cs @@ -0,0 +1,30 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace WinUIEx; + +/// +/// Class used to forward the IconElement property to the template. +/// +public class TitleBarTemplateSettings : DependencyObject +{ + /// + /// Initializes a new instance of the class. + /// + public TitleBarTemplateSettings() + { + } + + /// + /// Gets or sets the IconElement property + /// + public IconElement? IconElement + { + get { return (IconElement?)GetValue(IconElementProperty); } + set { SetValue(IconElementProperty, value); } + } + + /// Identifies the dependency property. + public static readonly DependencyProperty IconElementProperty = + DependencyProperty.Register("IconElement", typeof(IconElement), typeof(TitleBarTemplateSettings), new PropertyMetadata(null)); +} diff --git a/src/WinUIEx/TitleBar/TitleBar_themeresources.xaml b/src/WinUIEx/TitleBar/TitleBar_themeresources.xaml new file mode 100644 index 0000000..d51dbf1 --- /dev/null +++ b/src/WinUIEx/TitleBar/TitleBar_themeresources.xaml @@ -0,0 +1,192 @@ + + + + + + + + + + #FFFFFF + #FFFFFF + #CFCFCF + #717171 + + + + 32 + 48 + + + + + + + #191919 + #191919 + #606060 + #9b9b9b + + + + 32 + 48 + + + + + + + + + + + + + + 32 + 48 + + + + 0.5 + + + + + diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index e779a1d..df9fa6f 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -10,7 +10,7 @@ AnyCPU enable README.md - 10 + 12 true true true diff --git a/src/WinUIExSample/MainWindow.xaml b/src/WinUIExSample/MainWindow.xaml index 5e0b19b..f35afcd 100644 --- a/src/WinUIExSample/MainWindow.xaml +++ b/src/WinUIExSample/MainWindow.xaml @@ -11,20 +11,24 @@ Width="1024" Height="768" MinWidth="500" MinHeight="250" mc:Ignorable="d" > - + - - + + - - - - - - + + + + + + + + + + IsPaneToggleButtonVisible="False" CompactModeThresholdWidth="0" x:Name="navigationView" + OpenPaneLength="250" Grid.Row="1" IsBackButtonVisible="Collapsed" PaneDisplayMode="Auto" SelectionChanged="NavigationView_SelectionChanged"> diff --git a/src/WinUIExSample/MainWindow.xaml.cs b/src/WinUIExSample/MainWindow.xaml.cs index 2789fff..7c1b290 100644 --- a/src/WinUIExSample/MainWindow.xaml.cs +++ b/src/WinUIExSample/MainWindow.xaml.cs @@ -49,6 +49,7 @@ private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.Navigati if (args.IsSettingsSelected) { contentFrame.Navigate(typeof(Pages.Settings)); + titleBar.Subtitle = "Settings"; } else { @@ -57,6 +58,7 @@ private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.Navigati { string selectedItemTag = ((string)selectedItem.Tag); sender.Header = selectedItem.Content; + titleBar.Subtitle = selectedItem.Content as string; string pageName = "WinUIExSample.Pages." + selectedItemTag; Type pageType = Type.GetType(pageName); contentFrame.Navigate(pageType); @@ -102,11 +104,11 @@ public void ShowLogWindow() if (logWindow is null || logWindow.AppWindow is null) { logWindow = new LogWindow(); - logWindow.Closed += (s,e) => this.logWindow = null; + logWindow.Closed += (s, e) => this.logWindow = null; } logWindow.Activate(); } - + public void ToggleWMMessages(bool isOn) { @@ -119,7 +121,7 @@ public void ToggleWMMessages(bool isOn) private void WindowMessageReceived(object sender, WindowMessageEventArgs e) { Log(e.Message.ToString()); - if(e.Message.MessageId == 0x0005) //WM_SIZE + if (e.Message.MessageId == 0x0005) //WM_SIZE { // https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-size switch (e.Message.WParam) @@ -135,7 +137,18 @@ private void WindowMessageReceived(object sender, WindowMessageEventArgs e) private void NavigationView_BackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args) { - if(contentFrame.CanGoBack) + if (contentFrame.CanGoBack) + contentFrame.GoBack(); + } + + private void TitleBar_PaneToggleRequested(TitleBar sender, object args) + { + navigationView.IsPaneOpen = !navigationView.IsPaneOpen; + } + + private void TitleBar_BackRequested(TitleBar sender, object args) + { + if (contentFrame.CanGoBack) contentFrame.GoBack(); } }