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;