From fe1521221fe92f7edec9de944991d6968e5809b2 Mon Sep 17 00:00:00 2001 From: SQReder Date: Mon, 21 Sep 2026 18:12:12 +0300 Subject: [PATCH] Close the window "more options" menu when the cursor leaves The overlay window never activates, so the context menu gets no deactivation to dismiss itself on. It was closed from a MouseLeave handler that was only attached after the cursor had entered the menu, so a menu the cursor never entered stayed open indefinitely, even after its window was gone. Poll the real cursor location while the menu is open and close it once the cursor has left both the menu and its button. IsMouseOver cannot be used because the open menu captures the mouse. Also close the menu when the control is unloaded, and keep the actions bar visible for as long as the menu is open. --- FancyWM/Controls/TilingWindow.xaml.cs | 68 ++++++++++++++++----- FancyWM/ViewModels/TilingWindowViewModel.cs | 32 +++++++++- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/FancyWM/Controls/TilingWindow.xaml.cs b/FancyWM/Controls/TilingWindow.xaml.cs index 52e3a5b..b794581 100644 --- a/FancyWM/Controls/TilingWindow.xaml.cs +++ b/FancyWM/Controls/TilingWindow.xaml.cs @@ -1,8 +1,10 @@ -using System.Windows; +using System; +using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; +using System.Windows.Threading; +using FancyWM.Layouts.Tiling; using FancyWM.ViewModels; namespace FancyWM.Controls @@ -25,9 +27,23 @@ public TilingWindowViewModel ViewModel set => SetValue(ViewModelProperty, value); } + // The overlay window never activates, so the menu gets no deactivation + // to close itself on. Close it once the cursor has left it instead. + private const int MenuCloseAfterMissedTicks = 2; + private readonly DispatcherTimer m_menuWatchTimer = new() { Interval = TimeSpan.FromMilliseconds(150) }; + private int m_menuMissedTicks; + public TilingWindow() { InitializeComponent(); + m_menuWatchTimer.Tick += OnMenuWatchTick; + MoreContextMenu.Opened += (_, _) => SetMenuOpen(ViewModel, true); + MoreContextMenu.Closed += (_, _) => + { + m_menuWatchTimer.Stop(); + SetMenuOpen(ViewModel, false); + }; + Unloaded += (_, _) => MoreContextMenu.IsOpen = false; } protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) @@ -37,6 +53,15 @@ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { DataContext = ViewModel; MoreContextMenu.IsOpen = false; + SetMenuOpen(e.OldValue as TilingWindowViewModel, false); + } + } + + private static void SetMenuOpen(TilingWindowViewModel? viewModel, bool isOpen) + { + if (viewModel != null) + { + viewModel.IsMenuOpen = isOpen; } } @@ -44,24 +69,39 @@ private void OnMoreClick(object sender, RoutedEventArgs e) { MoreContextMenu.IsOpen = true; MoreContextMenu.DataContext = ViewModel; - var child = (UIElement)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(MoreContextMenu, 0), 0); - child.MouseEnter -= OnContextMenuMouseEnter; - child.MouseEnter += OnContextMenuMouseEnter; + m_menuMissedTicks = 0; + m_menuWatchTimer.Start(); } - private void OnContextMenuMouseEnter(object sender, MouseEventArgs e) + private void OnMenuWatchTick(object? sender, EventArgs e) { - var child = (UIElement)sender; - child.MouseEnter -= OnContextMenuMouseEnter; - child.MouseLeave -= OnContextMenuMouseLeave; - child.MouseLeave += OnContextMenuMouseLeave; + if (IsCursorOver(MoreContextMenu) || IsCursorOver(MoreButton)) + { + m_menuMissedTicks = 0; + return; + } + + if (++m_menuMissedTicks >= MenuCloseAfterMissedTicks) + { + MoreContextMenu.IsOpen = false; + } } - private void OnContextMenuMouseLeave(object sender, MouseEventArgs e) + /// + /// IsMouseOver is useless here: the open menu captures the mouse, + /// and WPF reports the capturing element as the one under the cursor. + /// + private bool IsCursorOver(FrameworkElement element) { - var child = (UIElement)sender; - child.MouseLeave -= OnContextMenuMouseLeave; - MoreContextMenu.IsOpen = false; + if (ViewModel?.Node is not WindowNode node || PresentationSource.FromVisual(element) == null) + { + return false; + } + + var cursor = node.WindowReference.Workspace.CursorLocation; + var local = element.PointFromScreen(new Point(cursor.X, cursor.Y)); + return 0 <= local.X && local.X <= element.ActualWidth + && 0 <= local.Y && local.Y <= element.ActualHeight; } bool m_canTriggerHorizontalGroup = true; diff --git a/FancyWM/ViewModels/TilingWindowViewModel.cs b/FancyWM/ViewModels/TilingWindowViewModel.cs index c9732dc..32e8e27 100644 --- a/FancyWM/ViewModels/TilingWindowViewModel.cs +++ b/FancyWM/ViewModels/TilingWindowViewModel.cs @@ -38,6 +38,26 @@ private enum RevealState public double RevealHighlightOpacity { get => m_revealHighlightOpacity; set => SetField(ref m_revealHighlightOpacity, value); } public bool IsActionActive { get => m_isActionActive; set => SetField(ref m_isActionActive, value); } + /// + /// Keeps the actions visible while their "more options" menu is open. + /// + public bool IsMenuOpen + { + get => m_isMenuOpen; + set + { + if (m_isMenuOpen == value) + { + return; + } + SetField(ref m_isMenuOpen, value); + // The cursor may be resting far away by the time the menu closes. + if (!value && m_workspace != null) + { + UpdateActionsReveal(m_workspace.CursorLocation); + } + } + } public bool IsPreviewVisible { get => m_isPreviewVisible; set => SetField(ref m_isPreviewVisible, value); } private IWorkspace? m_workspace; @@ -50,6 +70,7 @@ private enum RevealState private bool m_isMoving = false; private bool m_isPreviewVisible = false; private bool m_isActionActive = false; + private bool m_isMenuOpen = false; private WindowNode? m_currentNode; public event RoutedEventHandler? BeginHorizontalSplitWith; @@ -164,10 +185,15 @@ private void WindowReference_PositionChangeStart(object? sender, WindowPositionC } private void OnCursorLocationChanged(object? sender, CursorLocationChangedEventArgs e) + { + UpdateActionsReveal(e.NewLocation); + } + + private void UpdateActionsReveal(WinMan.Point cursorLocation) { if (Node is WindowNode node) { - if (m_isActionActive) + if (m_isActionActive || m_isMenuOpen) { RevealHighlightOpacity = 0; ActionsVisibility = Visibility.Visible; @@ -185,8 +211,8 @@ private void OnCursorLocationChanged(object? sender, CursorLocationChangedEventA var windowPos = node.WindowReference.Position; - var x = e.NewLocation.X - windowPos.Left; - var y = e.NewLocation.Y - windowPos.Top; + var x = cursorLocation.X - windowPos.Left; + var y = cursorLocation.Y - windowPos.Top; var isInBoundsX = 0 <= x && x <= windowPos.Width;