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;