Skip to content

Commit 1c1eea6

Browse files
authored
Second window refinements 2 (#227)
* fix(secondwindow): open WinUI context menu on first right-click - show the second-window flyout directly from ShowContextMenuInSecondWindowMode - guard flyout display behind IsContextMenuVisible during window activation - require second-window content before attempting to show the flyout * fix(secondwindow): place context menu outside taskbar work area - pass a tray-sized exclusion rectangle to CalculatePopupWindowPosition - include TPM_WORKAREA when calculating popup placement - keep menu placement adjacent to the tray icon instead of overlapping the taskbar band * feat(secondwindow): expose open event and support owner window linkage - add SecondWindowContextMenuOpened event when the flyout opens - add HwndUtilities.SetOwnerWindow for popup ownership via GWLP_HWNDPARENT - link second-window popup ownership to the tray message window
1 parent 6595c6f commit 1c1eea6

3 files changed

Lines changed: 104 additions & 7 deletions

File tree

src/libs/H.NotifyIcon.Shared/TaskbarIcon.ContextMenu.WinUI.SecondWindow.cs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public partial class TaskbarIcon
1515
private nint? ContextMenuWindowHandle { get; set; }
1616
private AppWindow? ContextMenuAppWindow { get; set; }
1717
private MenuFlyout? ContextMenuFlyout { get; set; }
18+
public event EventHandler? SecondWindowContextMenuOpened;
1819

1920
#pragma warning disable CA1822 // Mark members as static
2021
partial void OnContextMenuModeChanged(ContextMenuMode oldValue, ContextMenuMode newValue)
@@ -33,21 +34,57 @@ partial void OnContextMenuModeChanged(ContextMenuMode oldValue, ContextMenuMode
3334
private void ShowContextMenuInSecondWindowMode(System.Drawing.Point cursorPosition)
3435
{
3536
if (ContextMenuWindowHandle == null ||
36-
ContextMenuFlyout == null)
37+
ContextMenuFlyout == null ||
38+
ContextMenuWindow?.Content == null)
3739
{
3840
return;
3941
}
4042

4143
var size = MeasureFlyout(ContextMenuFlyout, new Size(10000.0, 10000.0));
44+
var excludeRect = CreateTrayCursorExcludeRect(cursorPosition);
4245
var rectangle = CursorUtilities.CalculatePopupWindowPosition(
4346
cursorPosition.X,
4447
cursorPosition.Y,
4548
(int)size.Width,
46-
(int)size.Height);
49+
(int)size.Height,
50+
excludeRect);
4751

52+
IsContextMenuVisible = true;
4853
ContextMenuAppWindow?.MoveAndResize(rectangle.ToRectInt32());
4954
_ = WindowUtilities.ShowWindow(ContextMenuWindowHandle.Value);
5055
_ = WindowUtilities.SetForegroundWindow(ContextMenuWindowHandle.Value);
56+
ShowSecondWindowFlyout(ContextMenuFlyout, ContextMenuWindow.Content);
57+
}
58+
59+
private static System.Drawing.Rectangle CreateTrayCursorExcludeRect(System.Drawing.Point cursorPosition)
60+
{
61+
// Native tray menus avoid overlapping the icon/taskbar affordance itself.
62+
// Give CalculatePopupWindowPosition a small tray-sized exclusion box so it
63+
// picks a position adjacent to the cursor instead of overlapping the taskbar.
64+
const int width = 36;
65+
const int height = 36;
66+
67+
return new System.Drawing.Rectangle(
68+
x: cursorPosition.X - (width / 2),
69+
y: cursorPosition.Y - (height / 2),
70+
width: width,
71+
height: height);
72+
}
73+
74+
private static void ShowSecondWindowFlyout(MenuFlyout flyout, UIElement target)
75+
{
76+
if (!flyout.IsOpen)
77+
{
78+
flyout.ShowAt(target, new FlyoutShowOptions
79+
{
80+
ShowMode = FlyoutShowMode.Transient,
81+
});
82+
}
83+
}
84+
85+
private void RaiseSecondWindowContextMenuOpened()
86+
{
87+
SecondWindowContextMenuOpened?.Invoke(this, EventArgs.Empty);
5188
}
5289

5390
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicConstructors, typeof(OverlappedPresenter))]
@@ -86,6 +123,13 @@ private void PrepareContextMenuWindow()
86123
DesktopWindowsManagerMethods.SetRoundedCorners(handle);
87124
WindowUtilities.MakeTransparent(handle);
88125

126+
if (TrayIcon.WindowHandle != 0)
127+
{
128+
// Keep the second-window popup in the tray icon's window family so it
129+
// participates in the same z-order/activation stack as the tray host.
130+
HwndUtilities.SetOwnerWindow(handle, TrayIcon.WindowHandle);
131+
}
132+
89133
#if !HAS_UNO
90134
var id = Win32Interop.GetWindowIdFromWindow(handle);
91135
var appWindow = AppWindow.GetFromWindowId(id);
@@ -104,6 +148,13 @@ private void PrepareContextMenuWindow()
104148
AreOpenCloseAnimationsEnabled = ContextFlyout.AreOpenCloseAnimationsEnabled,
105149
Placement = FlyoutPlacementMode.Full,
106150
};
151+
flyout.Opened += (_, _) =>
152+
{
153+
if (IsContextMenuVisible)
154+
{
155+
RaiseSecondWindowContextMenuOpened();
156+
}
157+
};
107158
flyout.Closed += (_, _) =>
108159
{
109160
if (!flyout.AreOpenCloseAnimationsEnabled ||
@@ -168,11 +219,12 @@ private void PrepareContextMenuWindow()
168219
return;
169220
}
170221

171-
IsContextMenuVisible = true;
172-
flyout.ShowAt(window.Content, new FlyoutShowOptions
222+
if (!IsContextMenuVisible)
173223
{
174-
ShowMode = FlyoutShowMode.Transient,
175-
});
224+
return;
225+
}
226+
227+
ShowSecondWindowFlyout(flyout, window.Content);
176228
};
177229

178230
ContextMenuWindow = window;

src/libs/H.NotifyIcon/Core/CursorUtilities.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ public static unsafe Rectangle CalculatePopupWindowPosition(
6464
cx = width,
6565
cy = height,
6666
};
67-
var flags = TRACK_POPUP_MENU_FLAGS.TPM_BOTTOMALIGN;
67+
var flags =
68+
TRACK_POPUP_MENU_FLAGS.TPM_BOTTOMALIGN |
69+
TRACK_POPUP_MENU_FLAGS.TPM_WORKAREA;
6870
var _excludeRect = new RECT
6971
{
7072
left = excludeRect?.Left ?? 0,

src/libs/H.NotifyIcon/Interop/HwndUtilities.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,47 @@ public static void SetWindowStyleAsPopupWindow(IntPtr hWnd)
4545
{
4646
SetWindowStyle(hWnd, WINDOW_STYLE.WS_POPUPWINDOW);
4747
}
48+
49+
/// <summary>
50+
/// Sets the owner for a top-level popup window.
51+
/// </summary>
52+
/// <param name="hWnd">Window handle.</param>
53+
/// <param name="ownerHWnd">Owner window handle.</param>
54+
[SupportedOSPlatform("windows5.0")]
55+
public static void SetOwnerWindow(IntPtr hWnd, IntPtr ownerHWnd)
56+
{
57+
var window = new HWND(hWnd);
58+
59+
// GWLP_HWNDPARENT is not exposed consistently across all projections we build against.
60+
const WINDOW_LONG_PTR_INDEX ownerIndex = (WINDOW_LONG_PTR_INDEX)(-8);
61+
62+
Marshal.SetLastPInvokeError(0);
63+
if (Environment.Is64BitProcess)
64+
{
65+
_ = PInvoke.SetWindowLongPtr(window, ownerIndex, ownerHWnd);
66+
}
67+
else
68+
{
69+
_ = PInvoke.SetWindowLong(window, ownerIndex, ownerHWnd.ToInt32());
70+
}
71+
72+
var error = Marshal.GetLastPInvokeError();
73+
if (error != 0)
74+
{
75+
throw new Win32Exception(error);
76+
}
77+
78+
_ = PInvoke.SetWindowPos(
79+
hWnd: window,
80+
hWndInsertAfter: new HWND(IntPtr.Zero),
81+
X: 0,
82+
Y: 0,
83+
cx: 0,
84+
cy: 0,
85+
uFlags: SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED |
86+
SET_WINDOW_POS_FLAGS.SWP_NOMOVE |
87+
SET_WINDOW_POS_FLAGS.SWP_NOSIZE |
88+
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE |
89+
SET_WINDOW_POS_FLAGS.SWP_NOZORDER);
90+
}
4891
}

0 commit comments

Comments
 (0)