From 7513b4d83a01ec25cfc62fc806888a777f6aa1e3 Mon Sep 17 00:00:00 2001 From: Joe Du <13188169+joesdu@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:42:03 +0800 Subject: [PATCH 01/12] fix: Fix system tray right-click menu not displaying --- src/Wpf.Ui.Tray/Controls/NotifyIcon.cs | 36 +++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs index 32f1b2a6e..0b2091f2b 100644 --- a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs +++ b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs @@ -246,6 +246,9 @@ public NotifyIcon() internalNotifyIconManager = new Wpf.Ui.Tray.Internal.InternalNotifyIconManager(); RegisterHandlers(); + + // Listen for DataContext changes to update ContextMenu + DataContextChanged += OnDataContextChanged; } /// @@ -363,6 +366,9 @@ protected virtual void Dispose(bool disposing) System.Diagnostics.Debug.WriteLine($"INFO | {typeof(NotifyIcon)} disposed.", "Wpf.Ui.NotifyIcon"); + // Clean up event handlers + DataContextChanged -= OnDataContextChanged; + Unregister(); internalNotifyIconManager.Dispose(); @@ -375,6 +381,13 @@ protected virtual void Dispose(bool disposing) protected virtual void OnMenuChanged(ContextMenu contextMenu) { internalNotifyIconManager.ContextMenu = contextMenu; + + // Set the DataContext for ContextMenu to enable binding + if (contextMenu.DataContext == null && DataContext != null) + { + contextMenu.DataContext = DataContext; + } + internalNotifyIconManager.ContextMenu.SetCurrentValue(Control.FontSizeProperty, MenuFontSize); } @@ -410,11 +423,11 @@ private static void OnFocusOnLeftClickChanged(DependencyObject d, DependencyProp if (e.NewValue is not bool newValue) { notifyIcon.FocusOnLeftClick = false; - + notifyIcon.internalNotifyIconManager.FocusOnLeftClick = false; return; } - notifyIcon.FocusOnLeftClick = newValue; + notifyIcon.internalNotifyIconManager.FocusOnLeftClick = newValue; } private static void OnMenuOnRightClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -427,11 +440,11 @@ private static void OnMenuOnRightClickChanged(DependencyObject d, DependencyProp if (e.NewValue is not bool newValue) { notifyIcon.MenuOnRightClick = false; - + notifyIcon.internalNotifyIconManager.MenuOnRightClick = false; return; } - notifyIcon.MenuOnRightClick = newValue; + notifyIcon.internalNotifyIconManager.MenuOnRightClick = newValue; } private static void OnMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -455,6 +468,12 @@ private void InitializeIcon() internalNotifyIconManager.Icon = Icon; internalNotifyIconManager.MenuOnRightClick = MenuOnRightClick; internalNotifyIconManager.FocusOnLeftClick = FocusOnLeftClick; + + // Add Menu initialization + if (Menu != null) + { + OnMenuChanged(Menu); + } } private void RegisterHandlers() @@ -466,4 +485,13 @@ private void RegisterHandlers() internalNotifyIconManager.MiddleClick += OnMiddleClick; internalNotifyIconManager.MiddleDoubleClick += OnMiddleDoubleClick; } + + private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + // Update ContextMenu DataContext when NotifyIcon DataContext changes + if (Menu != null && e.NewValue != null) + { + Menu.DataContext = e.NewValue; + } + } } From f399109868373e46f8172be93b9c46b424f2d13b Mon Sep 17 00:00:00 2001 From: Joe Du <13188169+joesdu@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:42:36 +0800 Subject: [PATCH 02/12] fix: Fix system tray right-click menu not displaying --- src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs b/src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs index fdd299344..1f7d9267f 100644 --- a/src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs +++ b/src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs @@ -178,7 +178,10 @@ protected virtual void OpenMenu() // Without setting the handler window at the front, menu may appear behind the taskbar _ = Interop.User32.SetForegroundWindow(HookWindow.Handle); - ContextMenuService.SetPlacement(ContextMenu, PlacementMode.MousePoint); + + // Set placement properties for better positioning + ContextMenu.SetCurrentValue(ContextMenu.PlacementProperty, PlacementMode.MousePoint); + ContextMenu.SetCurrentValue(ContextMenu.PlacementTargetProperty, null); // ContextMenu.ApplyMica(); ContextMenu.SetCurrentValue(ContextMenu.IsOpenProperty, true); From 407812a608365855f832aed5f2582bce09cf758d Mon Sep 17 00:00:00 2001 From: Joe Du <13188169+joesdu@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:51:48 +0800 Subject: [PATCH 03/12] Update src/Wpf.Ui.Tray/Controls/NotifyIcon.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Wpf.Ui.Tray/Controls/NotifyIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs index 0b2091f2b..f74b94dfd 100644 --- a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs +++ b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs @@ -427,7 +427,7 @@ private static void OnFocusOnLeftClickChanged(DependencyObject d, DependencyProp return; } - notifyIcon.internalNotifyIconManager.FocusOnLeftClick = newValue; + notifyIcon.FocusOnLeftClick = newValue; } private static void OnMenuOnRightClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) From c508dccc7a36d101e66b0ef55b0c01e4e449dc09 Mon Sep 17 00:00:00 2001 From: Joe Du <13188169+joesdu@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:51:57 +0800 Subject: [PATCH 04/12] Update src/Wpf.Ui.Tray/Controls/NotifyIcon.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Wpf.Ui.Tray/Controls/NotifyIcon.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs index f74b94dfd..0b773ae82 100644 --- a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs +++ b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs @@ -440,7 +440,6 @@ private static void OnMenuOnRightClickChanged(DependencyObject d, DependencyProp if (e.NewValue is not bool newValue) { notifyIcon.MenuOnRightClick = false; - notifyIcon.internalNotifyIconManager.MenuOnRightClick = false; return; } From 2227c4784549e36d0730e2c006d776f7cf1a1fcc Mon Sep 17 00:00:00 2001 From: Joe Du <13188169+joesdu@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:52:03 +0800 Subject: [PATCH 05/12] Update src/Wpf.Ui.Tray/Controls/NotifyIcon.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Wpf.Ui.Tray/Controls/NotifyIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs index 0b773ae82..940ed5fd6 100644 --- a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs +++ b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs @@ -443,7 +443,7 @@ private static void OnMenuOnRightClickChanged(DependencyObject d, DependencyProp return; } - notifyIcon.internalNotifyIconManager.MenuOnRightClick = newValue; + notifyIcon.MenuOnRightClick = newValue; } private static void OnMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) From 49dfa3b1e8a1be312801e855ead743e6fa2f0916 Mon Sep 17 00:00:00 2001 From: Joe Du <13188169+joesdu@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:52:09 +0800 Subject: [PATCH 06/12] Update src/Wpf.Ui.Tray/Controls/NotifyIcon.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Wpf.Ui.Tray/Controls/NotifyIcon.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs index 940ed5fd6..729fe56f7 100644 --- a/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs +++ b/src/Wpf.Ui.Tray/Controls/NotifyIcon.cs @@ -423,7 +423,6 @@ private static void OnFocusOnLeftClickChanged(DependencyObject d, DependencyProp if (e.NewValue is not bool newValue) { notifyIcon.FocusOnLeftClick = false; - notifyIcon.internalNotifyIconManager.FocusOnLeftClick = false; return; } From 366209b78d964054f8fe9ffd939d15c5cbd958f9 Mon Sep 17 00:00:00 2001 From: Joes Date: Mon, 28 Jul 2025 12:01:14 +0800 Subject: [PATCH 07/12] feat: add tray menu demo --- .../ViewModels/Windows/MainWindowViewModel.cs | 25 ++++- .../Views/Windows/MainWindow.xaml.cs | 93 +++++++++++++++++++ 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs index 8132dd717..9edaa4c2f 100644 --- a/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs +++ b/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs @@ -63,7 +63,7 @@ public partial class MainWindowViewModel(IStringLocalizer localize new NavigationViewItem(nameof(ThumbRate), typeof(ThumbRatePage)), new NavigationViewItem(nameof(SplitButton), typeof(SplitButtonPage)), new NavigationViewItem(nameof(Slider), typeof(SliderPage)), - }, + } }, new NavigationViewItem { @@ -182,9 +182,26 @@ public partial class MainWindowViewModel(IStringLocalizer localize ]; [ObservableProperty] - private ObservableCollection _trayMenuItems = + private ObservableCollection _trayMenuItems = [ - new Wpf.Ui.Controls.MenuItem { Header = "Home", Tag = "tray_home" }, - new Wpf.Ui.Controls.MenuItem { Header = "Close", Tag = "tray_close" }, + new Wpf.Ui.Controls.MenuItem() + { + Header = "Home", + Tag = "tray_home", + Icon = new SymbolIcon { Symbol = SymbolRegular.Home24 } + }, + new Wpf.Ui.Controls.MenuItem() + { + Header = "Settings", + Tag = "tray_settings", + Icon = new SymbolIcon { Symbol = SymbolRegular.Settings24 } + }, + new Separator(), + new Wpf.Ui.Controls.MenuItem() + { + Header = "Close", + Tag = "tray_close", + Icon = new SymbolIcon { Symbol = SymbolRegular.Dismiss24 } + }, ]; } diff --git a/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml.cs b/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml.cs index cf2543f77..5dd0bdf6a 100644 --- a/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml.cs @@ -30,6 +30,7 @@ IContentDialogService contentDialogService snackbarService.SetSnackbarPresenter(SnackbarPresenter); navigationService.SetNavigationControl(NavigationView); contentDialogService.SetDialogHost(RootContentDialog); + SetupTrayMenuEvents(); } public MainWindowViewModel ViewModel { get; } @@ -38,6 +39,98 @@ IContentDialogService contentDialogService private bool _isPaneOpenedOrClosedFromCode; + private void SetupTrayMenuEvents() + { + foreach (var menuItem in ViewModel.TrayMenuItems) + { + if (menuItem is MenuItem item) + { + item.Click += OnTrayMenuItemClick; + } + } + } + + private void OnTrayMenuItemClick(object sender, RoutedEventArgs e) + { + if (sender is not Wpf.Ui.Controls.MenuItem menuItem) + { + return; + } + + var tag = menuItem.Tag?.ToString() ?? string.Empty; + + Debug.WriteLine($"System Tray Click: {menuItem.Header}, Tag: {tag}"); + + switch (tag) + { + case "tray_home": + HandleTrayHomeClick(); + break; + case "tray_settings": + HandleTraySettingsClick(); + break; + case "tray_close": + HandleTrayCloseClick(); + break; + default: + if (!string.IsNullOrEmpty(tag)) + { + System.Diagnostics.Debug.WriteLine($"unknown Tag: {tag}"); + } + + break; + } + } + + private void HandleTrayHomeClick() + { + System.Diagnostics.Debug.WriteLine("Tray menu - Home Click"); + + ShowAndActivateWindow(); + + NavigateToPage(typeof(DashboardPage)); + } + + private void HandleTraySettingsClick() + { + System.Diagnostics.Debug.WriteLine("Tray menu - Settings Click"); + + ShowAndActivateWindow(); + + NavigateToPage(typeof(SettingsPage)); + } + + private static void HandleTrayCloseClick() + { + System.Diagnostics.Debug.WriteLine("Tray menu - Close Click"); + + Application.Current.Shutdown(); + } + + private void ShowAndActivateWindow() + { + if (WindowState == WindowState.Minimized) + { + SetCurrentValue(WindowStateProperty, WindowState.Normal); + } + + Show(); + _ = Activate(); + _ = Focus(); + } + + private void NavigateToPage(Type pageType) + { + try + { + NavigationView.Navigate(pageType); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"NavigateToPage {pageType.Name} Error: {ex.Message}"); + } + } + private void OnNavigationSelectionChanged(object sender, RoutedEventArgs e) { if (sender is not Wpf.Ui.Controls.NavigationView navigationView) From 69489423314cd289dd6c8830879752607b5ca666 Mon Sep 17 00:00:00 2001 From: Joes Date: Wed, 6 Aug 2025 23:55:30 +0800 Subject: [PATCH 08/12] chore: enable ui:NavigationView.IsScrollable="False" --- .../NavigationView.AttachedProperties.cs | 35 +++++++++++++++++++ .../NavigationView.Navigation.cs | 24 +++++++++++++ .../NavigationView.TemplateParts.cs | 35 ++++++++++++++++++- .../NavigationViewContentPresenter.xaml | 3 +- 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/Wpf.Ui/Controls/NavigationView/NavigationView.AttachedProperties.cs b/src/Wpf.Ui/Controls/NavigationView/NavigationView.AttachedProperties.cs index 854580ef9..cea588276 100644 --- a/src/Wpf.Ui/Controls/NavigationView/NavigationView.AttachedProperties.cs +++ b/src/Wpf.Ui/Controls/NavigationView/NavigationView.AttachedProperties.cs @@ -45,6 +45,41 @@ private static void OnHeaderContentChanged(DependencyObject d, DependencyPropert public static void SetHeaderContent(FrameworkElement target, object? headerContent) => target.SetValue(HeaderContentProperty, headerContent); + // ============================================================ + // IsScrollable Attached Property + // ============================================================ + + /// + /// Identifies the attached property. + /// + public static readonly DependencyProperty IsScrollableProperty = DependencyProperty.RegisterAttached( + "IsScrollable", + typeof(bool), + typeof(NavigationView), + new FrameworkPropertyMetadata(true) + ); + + /// + /// Gets the value of the attached property for a specified element. + /// + /// The element from which to read the property value. + /// if the content of the page hosted in should be scrollable; otherwise, . + [AttachedPropertyBrowsableForType(typeof(DependencyObject))] + public static bool GetIsScrollable(DependencyObject element) + { + return (bool)element.GetValue(IsScrollableProperty); + } + + /// + /// Sets the value of the attached property for a specified element. + /// + /// The element on which to set the property value. + /// The value to set. to disable scrolling on the content presenter. + public static void SetIsScrollable(DependencyObject element, bool value) + { + element.SetValue(IsScrollableProperty, value); + } + // ============================================================ // NavigationParent Attached Property // ============================================================ diff --git a/src/Wpf.Ui/Controls/NavigationView/NavigationView.Navigation.cs b/src/Wpf.Ui/Controls/NavigationView/NavigationView.Navigation.cs index a4dfefdef..56dcb1c52 100644 --- a/src/Wpf.Ui/Controls/NavigationView/NavigationView.Navigation.cs +++ b/src/Wpf.Ui/Controls/NavigationView/NavigationView.Navigation.cs @@ -7,6 +7,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; +using System.Windows.Controls; using Wpf.Ui.Abstractions; // ReSharper disable once CheckNamespace @@ -382,10 +383,33 @@ System.Windows.Navigation.NavigationEventArgs e _ = frame.RemoveBackEntry(); + // Update content ScrollViewer based on page's IsScrollable property + UpdateContentScrollViewer(e.Content); + /*var replaced = 1; ((NavigationViewContentPresenter)sender).JournalOwnership =*/ } + private void UpdateContentScrollViewer(object? content) + { + if (ContentScrollViewer is null) + { + return; + } + + var isScrollable = true; + + if (content is DependencyObject dependencyObject) + { + isScrollable = NavigationView.GetIsScrollable(dependencyObject); + } + + ContentScrollViewer.SetCurrentValue( + ScrollViewer.VerticalScrollBarVisibilityProperty, + isScrollable ? ScrollBarVisibility.Auto : ScrollBarVisibility.Disabled + ); + } + private void AddToNavigationStack( INavigationViewItem viewItem, bool addToNavigationStack, diff --git a/src/Wpf.Ui/Controls/NavigationView/NavigationView.TemplateParts.cs b/src/Wpf.Ui/Controls/NavigationView/NavigationView.TemplateParts.cs index b0aa2436f..99fa3460d 100644 --- a/src/Wpf.Ui/Controls/NavigationView/NavigationView.TemplateParts.cs +++ b/src/Wpf.Ui/Controls/NavigationView/NavigationView.TemplateParts.cs @@ -6,7 +6,10 @@ // Based on Windows UI Library // Copyright(c) Microsoft Corporation.All rights reserved. +using System.Windows.Controls; + // ReSharper disable once CheckNamespace + namespace Wpf.Ui.Controls; /// @@ -30,10 +33,11 @@ namespace Wpf.Ui.Controls; Name = TemplateElementAutoSuggestBoxSymbolButton, Type = typeof(System.Windows.Controls.Button) )] +[TemplatePart(Name = TemplateElementContentScrollViewer, Type = typeof(ScrollViewer))] public partial class NavigationView { /// - /// Template element represented by the PART_MenuItemsItemsControl name. + /// Template element represented by the PART_NavigationViewContentPresenter name. /// private const string TemplateElementNavigationViewContentPresenter = "PART_NavigationViewContentPresenter"; @@ -63,6 +67,11 @@ public partial class NavigationView /// private const string TemplateElementAutoSuggestBoxSymbolButton = "PART_AutoSuggestBoxSymbolButton"; + /// + /// Template element represented by the PART_ContentScrollViewer name. + /// + private const string TemplateElementContentScrollViewer = "PART_ContentScrollViewer"; + /// /// Gets or sets the control responsible for rendering the content. /// @@ -93,6 +102,11 @@ public partial class NavigationView /// protected System.Windows.Controls.Button? AutoSuggestBoxSymbolButton { get; set; } + /// + /// Gets or sets the that hosts the content of the . + /// + protected ScrollViewer? ContentScrollViewer { get; set; } + /// public override void OnApplyTemplate() { @@ -121,6 +135,11 @@ public override void OnApplyTemplate() { NavigationViewContentPresenter.Navigated -= OnNavigationViewContentPresenterNavigated; NavigationViewContentPresenter.Navigated += OnNavigationViewContentPresenterNavigated; + + // We need to wait for the NavigationViewContentPresenter to apply its template + // so we can find the ContentScrollViewer + NavigationViewContentPresenter.ApplyTemplate(); + FindContentScrollViewer(); } if ( @@ -151,6 +170,20 @@ is System.Windows.Controls.Button autoSuggestBoxSymbolButton } } + private void FindContentScrollViewer() + { + if (NavigationViewContentPresenter?.Template is null) + { + return; + } + + // Try to find the ContentScrollViewer within the NavigationViewContentPresenter's template + if (NavigationViewContentPresenter.Template.FindName(TemplateElementContentScrollViewer, NavigationViewContentPresenter) is ScrollViewer scrollViewer) + { + ContentScrollViewer = scrollViewer; + } + } + protected T GetTemplateChild(string name) where T : DependencyObject { diff --git a/src/Wpf.Ui/Controls/NavigationView/NavigationViewContentPresenter.xaml b/src/Wpf.Ui/Controls/NavigationView/NavigationViewContentPresenter.xaml index b249c26c6..cb439ea76 100644 --- a/src/Wpf.Ui/Controls/NavigationView/NavigationViewContentPresenter.xaml +++ b/src/Wpf.Ui/Controls/NavigationView/NavigationViewContentPresenter.xaml @@ -1,4 +1,4 @@ -