| File: System\Windows\Forms\Controls\ToolStrips\ToolStripManager.ModalMenuFilter.cs | Web Access |
| Project: ..\..\..\src\winforms\src\System.Windows.Forms\System.Windows.Forms.csproj (System.Windows.Forms) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace System.Windows.Forms; public static partial class ToolStripManager { /// <remarks> /// <para> /// This installs a message filter when a dropdown becomes active. The filter: /// </para> /// <list type="bullet"> /// <item> /// <description> /// Eats WM_MOUSEMOVEs so that the window underneath doesn't get highlight processing/tooltips. /// </description> /// </item> /// <item><description>Dismisses the menu if clicked outside the dropdown.</description></item> /// <item><description>Dismisses all dropdowns if the active window changes.</description></item> /// <item><description>Redirects keyboard messages to the active dropdown.</description></item> /// </list> /// <para> /// There should be one Message Filter per thread and it should be uninstalled once the last dropdown has gone away. /// This is not part of <see cref="ToolStripManager"/> because it is DropDown specific and /// we don't want to publicly expose this message filter. /// </para> /// </remarks> internal partial class ModalMenuFilter : IMessageModifyAndFilter { // The window that was active when we showed the dropdown private HandleRef<HWND> _activeHwnd; // The window that was last known to be active private HandleRef<HWND> _lastActiveWindow; private List<ToolStrip>? _inputFilterQueue; private bool _inMenuMode; private bool _caretHidden; private bool _showUnderlines; private bool _menuKeyToggle; private bool _suspendMenuMode; private HostedWindowsFormsMessageHook? _messageHook; private Timer? _ensureMessageProcessingTimer; private const int MessageProcessingInterval = 500; private ToolStrip? _toplevelToolStrip; private readonly WeakReference<IKeyboardToolTip?> _lastFocusedTool = new(null); [ThreadStatic] private static ModalMenuFilter? t_instance; internal static ModalMenuFilter Instance => t_instance ??= new ModalMenuFilter(); private ModalMenuFilter() { } /// <summary> /// The HWnd that was active when we popped the first dropdown. /// </summary> internal static HandleRef<HWND> ActiveHwnd => Instance.ActiveHwndInternal; // returns whether or not we should show focus cues for mnemonics. public bool ShowUnderlines { get => _showUnderlines; set { if (_showUnderlines != value) { _showUnderlines = value; NotifyMenuModeChange(invalidateText: true, activationChange: false); } } } private HandleRef<HWND> ActiveHwndInternal { get => _activeHwnd; set { if (_activeHwnd.Handle != value.Handle) { Control? control = null; // Unsubscribe from handle recreate. if (_activeHwnd.Handle != IntPtr.Zero) { control = Control.FromHandle(_activeHwnd.Handle); control?.HandleCreated -= OnActiveHwndHandleCreated; } _activeHwnd = value; // make sure we watch out for handle recreates. control = Control.FromHandle(_activeHwnd.Handle); control?.HandleCreated += OnActiveHwndHandleCreated; } } } /// <summary> /// Returns whether or not someone has called EnterMenuMode. /// </summary> internal static bool InMenuMode => Instance._inMenuMode; internal static bool MenuKeyToggle { get => Instance._menuKeyToggle; set { if (Instance._menuKeyToggle != value) { Instance._menuKeyToggle = value; } } } /// <summary> /// Used in scenarios where windows forms does not own the message pump, /// but needs access to the message queue. /// </summary> private HostedWindowsFormsMessageHook MessageHook => _messageHook ??= new HostedWindowsFormsMessageHook(); // ToolStrip analog to WM_ENTERMENULOOP private void EnterMenuModeCore() { Debug.Assert(!InMenuMode, "How did we get here if we're already in menu mode?"); if (!InMenuMode) { HWND hwndActive = PInvoke.GetActiveWindow(); if (!hwndActive.IsNull) { ActiveHwndInternal = new(Control.FromHandle(hwndActive), hwndActive); } Application.ThreadContext.FromCurrent().AddMessageFilter(this); Application.ThreadContext.FromCurrent().TrackInput(true); if (!Application.ThreadContext.FromCurrent().GetMessageLoop(true)) { // Message filter isn't going to help as we don't own the message pump // switch over to a MessageHook MessageHook.HookMessages = true; } _inMenuMode = true; NotifyLastLastFocusedToolAboutFocusLoss(); // Fire timer messages to force our filter to get evaluated. ProcessMessages(true); } } internal void NotifyLastLastFocusedToolAboutFocusLoss() { IKeyboardToolTip? lastFocusedTool = KeyboardToolTipStateMachine.Instance.LastFocusedTool; if (lastFocusedTool is not null) { _lastFocusedTool.SetTarget(lastFocusedTool); KeyboardToolTipStateMachine.Instance.NotifyAboutLostFocus(lastFocusedTool); } } internal static void ExitMenuMode() => Instance.ExitMenuModeCore(); private void ExitMenuModeCore() { // Ensure we've cleaned up the timer. ProcessMessages(process: false); if (!InMenuMode) { return; } try { // Message filter isn't going to help as we don't own the message pump // Switch over to a MessageHook _messageHook?.HookMessages = false; Application.ThreadContext.FromCurrent().RemoveMessageFilter(this); Application.ThreadContext.FromCurrent().TrackInput(false); if (!ActiveHwnd.Handle.IsNull) { // Unsubscribe from handle creates Control? control = Control.FromHandle(ActiveHwnd.Handle); control?.HandleCreated -= OnActiveHwndHandleCreated; ActiveHwndInternal = default; } _inputFilterQueue?.Clear(); if (_caretHidden) { _caretHidden = false; PInvoke.ShowCaret(HWND.Null); } if (_lastFocusedTool.TryGetTarget(out IKeyboardToolTip? tool) && tool is not null) { KeyboardToolTipStateMachine.Instance.NotifyAboutGotFocus(tool); } } finally { _inMenuMode = false; // Skip the setter here so we only iterate through the toolstrips once. bool textStyleChanged = _showUnderlines; _showUnderlines = false; NotifyMenuModeChange(invalidateText: textStyleChanged, activationChange: true); } } internal static ToolStrip? GetActiveToolStrip() => Instance.GetActiveToolStripInternal(); internal ToolStrip? GetActiveToolStripInternal() { if (_inputFilterQueue is not null && _inputFilterQueue.Count > 0) { return _inputFilterQueue[^1]; } return null; } /// <summary> /// Returns the ToolStrip that is at the root. /// </summary> private ToolStrip? GetCurrentTopLevelToolStrip() { if (_toplevelToolStrip is null) { ToolStrip? activeToolStrip = GetActiveToolStripInternal(); if (activeToolStrip is not null) { _toplevelToolStrip = activeToolStrip.GetToplevelOwnerToolStrip(); } } return _toplevelToolStrip; } private void OnActiveHwndHandleCreated(object? sender, EventArgs e) { ActiveHwndInternal = new(sender as Control); } internal static void ProcessMenuKeyDown(ref Message m) { Keys keyData = (Keys)(nint)m.WParamInternal; if (Control.FromHandle(m.HWnd) is ToolStrip toolStrip && !toolStrip.IsDropDown) { return; } // Handle the case where the ALT key has been pressed down while a dropdown was open. We need to clear // off the MenuKeyToggle so the next ALT will activate the menu. if (IsMenuKey(keyData)) { if (!InMenuMode && MenuKeyToggle) { MenuKeyToggle = false; } else if (!MenuKeyToggle) { Instance.ShowUnderlines = true; } } } internal static void CloseActiveDropDown(ToolStripDropDown activeToolStripDropDown, ToolStripDropDownCloseReason reason) { activeToolStripDropDown.SetCloseReason(reason); activeToolStripDropDown.Visible = false; // There's no more dropdowns left in the chain if (GetActiveToolStrip() is null) { ExitMenuMode(); // Make sure we roll selection off the toplevel toolstrip. activeToolStripDropDown.OwnerItem?.Unselect(); } } /// <summary> /// Fire a timer event to ensure we have a message in the queue every 500ms /// </summary> private void ProcessMessages(bool process) { if (process) { _ensureMessageProcessingTimer ??= new Timer(); _ensureMessageProcessingTimer.Interval = MessageProcessingInterval; _ensureMessageProcessingTimer.Enabled = true; } else if (_ensureMessageProcessingTimer is not null) { _ensureMessageProcessingTimer.Enabled = false; _ensureMessageProcessingTimer.Dispose(); _ensureMessageProcessingTimer = null; } } private void ProcessMouseButtonPressed(HWND hwndMouseMessageIsFrom, Point location) { int countDropDowns = _inputFilterQueue?.Count ?? 0; for (int i = 0; i < countDropDowns; i++) { ToolStrip? activeToolStrip = GetActiveToolStripInternal(); if (activeToolStrip is not null) { Point translatedLocation = location; PInvokeCore.MapWindowPoints(hwndMouseMessageIsFrom, activeToolStrip, ref translatedLocation); if (!activeToolStrip.ClientRectangle.Contains(translatedLocation)) { if (activeToolStrip is ToolStripDropDown activeToolStripDropDown) { if (!(activeToolStripDropDown.OwnerToolStrip is not null && activeToolStripDropDown.OwnerToolStrip.HWND == hwndMouseMessageIsFrom && activeToolStripDropDown.OwnerDropDownItem is not null && activeToolStripDropDown.OwnerDropDownItem.DropDownButtonArea.Contains(location))) { // The owner item should handle closing the dropdown // this allows code such as if (DropDown.Visible) { Hide, Show } etc. CloseActiveDropDown(activeToolStripDropDown, ToolStripDropDownCloseReason.AppClicked); } } else { // Make sure we clear the selection. activeToolStrip.NotifySelectionChange(item: null); // We're a toplevel toolstrip and we've clicked somewhere else. Exit menu mode. ExitMenuModeCore(); } } else { // We've found a dropdown that intersects with the mouse message break; } } else { break; } } } private bool ProcessActivationChange() { int countDropDowns = _inputFilterQueue?.Count ?? 0; for (int i = 0; i < countDropDowns; i++) { if (GetActiveToolStripInternal() is ToolStripDropDown activeDropDown && activeDropDown.AutoClose) { activeDropDown.Visible = false; } } ExitMenuModeCore(); return true; } internal static void SetActiveToolStrip(ToolStrip toolStrip, bool menuKeyPressed) { if (!InMenuMode && menuKeyPressed) { Instance.ShowUnderlines = true; } Instance.SetActiveToolStripCore(toolStrip); } internal static void SetActiveToolStrip(ToolStrip toolStrip) => Instance.SetActiveToolStripCore(toolStrip); private void SetActiveToolStripCore(ToolStrip toolStrip) { if (toolStrip is null) { return; } if (toolStrip.IsDropDown) { // For something that never closes, don't use menu mode. ToolStripDropDown dropDown = (ToolStripDropDown)toolStrip; if (!dropDown.AutoClose) { // Store off the current active hwnd HWND hwndActive = PInvoke.GetActiveWindow(); if (!hwndActive.IsNull) { ActiveHwndInternal = new(Control.FromHandle(hwndActive), hwndActive); } // Don't actually enter menu mode. return; } } toolStrip.KeyboardActive = true; if (_inputFilterQueue is null) { // Use list because we want to be able to remove at any point. _inputFilterQueue = []; } else { ToolStrip? currentActiveToolStrip = GetActiveToolStripInternal(); // ToolStrip dropdowns push/pull their activation based on visibility. // we have to account for the toolstrips that aren't dropdowns if (currentActiveToolStrip is not null) { if (!currentActiveToolStrip.IsDropDown) { _inputFilterQueue.Remove(currentActiveToolStrip); } else if (toolStrip.IsDropDown && (ToolStripDropDown.GetFirstDropDown(toolStrip) != ToolStripDropDown.GetFirstDropDown(currentActiveToolStrip))) { _inputFilterQueue.Remove(currentActiveToolStrip); ToolStripDropDown currentActiveToolStripDropDown = (ToolStripDropDown)currentActiveToolStrip; currentActiveToolStripDropDown.DismissAll(); } } } // Reset the toplevel toolstrip _toplevelToolStrip = null; if (!_inputFilterQueue.Contains(toolStrip)) { _inputFilterQueue.Add(toolStrip); } if (!InMenuMode && _inputFilterQueue.Count > 0) { EnterMenuModeCore(); } // Hide the caret if we're showing a toolstrip dropdown if (!_caretHidden && toolStrip.IsDropDown && InMenuMode) { _caretHidden = true; PInvoke.HideCaret(HWND.Null); } } internal static void SuspendMenuMode() { Instance._suspendMenuMode = true; } internal static void ResumeMenuMode() { Instance._suspendMenuMode = false; } internal static void RemoveActiveToolStrip(ToolStrip toolStrip) { Instance.RemoveActiveToolStripCore(toolStrip); } private void RemoveActiveToolStripCore(ToolStrip toolStrip) { // Precautionary - remove the active toplevel toolstrip. _toplevelToolStrip = null; _inputFilterQueue?.Remove(toolStrip); } private static bool IsChildOrSameWindow<T>(in T hwndParent, in T hwndChild) where T : IHandle<HWND> => hwndParent.Handle == hwndChild.Handle || PInvoke.IsChild(hwndParent, hwndChild); private static bool IsKeyOrMouseMessage(Message m) { if (m.IsMouseMessage()) { return true; } else if (m.Msg is >= ((int)PInvokeCore.WM_NCLBUTTONDOWN) and <= ((int)PInvokeCore.WM_NCMBUTTONDBLCLK)) { return true; } else if (m.IsKeyMessage()) { return true; } return false; } public bool PreFilterMessage(ref Message m) { if (_suspendMenuMode) { return false; } ToolStrip? activeToolStrip = GetActiveToolStrip(); if (activeToolStrip is null) { return false; } if (activeToolStrip.IsDisposed) { RemoveActiveToolStripCore(activeToolStrip); return false; } HandleRef<HWND> activeToolStripHandle = new(activeToolStrip); var activeWindowHandle = Control.GetHandleRef(PInvoke.GetActiveWindow()); if (activeWindowHandle != _lastActiveWindow) { // If another window has gotten activation - we should dismiss. if (activeWindowHandle.Handle.IsNull) { // We don't know what it was cause it's on another thread or doesn't exist. ProcessActivationChange(); } else if (Control.FromChildHandle(activeWindowHandle.Handle) is not ToolStripDropDown && !IsChildOrSameWindow(activeWindowHandle, activeToolStripHandle) && !IsChildOrSameWindow(activeWindowHandle, ActiveHwnd)) { // Not a dropdown, and not a child of the active toolstrip or the active window. ProcessActivationChange(); } } // Store this off so we don't have to do activation processing next time _lastActiveWindow = activeWindowHandle; // Performance: skip over things like WM_PAINT. if (!IsKeyOrMouseMessage(m)) { return false; } DPI_AWARENESS_CONTEXT context = GetDpiAwarenessContextForWindow(m.HWND); using (ScaleHelper.EnterDpiAwarenessScope(context)) { switch (m.MsgInternal) { case PInvokeCore.WM_MOUSEMOVE: case PInvokeCore.WM_NCMOUSEMOVE: // Mouse move messages should be eaten if they aren't for a dropdown. // this prevents things like ToolTips and mouse over highlights from // being processed. Control? control = Control.FromChildHandle(m.HWnd); if (control is null || control.TopLevelControlInternal is not ToolStripDropDown) { // Double check it's not a child control of the active toolstrip. if (!IsChildOrSameWindow<IHandle<HWND>>(activeToolStripHandle, m)) { // It is NOT a child of the current active toolstrip. ToolStrip? toplevelToolStrip = GetCurrentTopLevelToolStrip(); if (toplevelToolStrip is not null && IsChildOrSameWindow<IHandle<HWND>>(toplevelToolStrip, m)) { // Don't eat mouse message. // The mouse message is from an HWND that is part of the toplevel toolstrip - let the mouse move through so // when you have something like the file menu open and mouse over the edit menu // the file menu will dismiss. return false; } else if (!IsChildOrSameWindow<IHandle<HWND>>(ActiveHwnd, m)) { // Don't eat mouse message. // the mouse message is from another toplevel HWND. return false; } // Eat mouse message // the HWND is // not part of the active toolstrip // not the toplevel toolstrip (e.g. MenuStrip). // not parented to the toplevel toolstrip (e.g a combo box on a menu strip). return true; } } break; case PInvokeCore.WM_LBUTTONDOWN: case PInvokeCore.WM_RBUTTONDOWN: case PInvokeCore.WM_MBUTTONDOWN: // When a mouse button is pressed, we should determine if it is within the client coordinates // of the active dropdown. If not, we should dismiss it. ProcessMouseButtonPressed(m.HWND, PARAM.ToPoint(m.LParamInternal)); break; case PInvokeCore.WM_NCLBUTTONDOWN: case PInvokeCore.WM_NCRBUTTONDOWN: case PInvokeCore.WM_NCMBUTTONDOWN: // When a mouse button is pressed, we should determine if it is within the client coordinates // of the active dropdown. If not, we should dismiss it. ProcessMouseButtonPressed(default, PARAM.ToPoint(m.LParamInternal)); break; case PInvokeCore.WM_KEYDOWN: case PInvokeCore.WM_KEYUP: case PInvokeCore.WM_CHAR: case PInvokeCore.WM_DEADCHAR: case PInvokeCore.WM_SYSKEYDOWN: case PInvokeCore.WM_SYSKEYUP: case PInvokeCore.WM_SYSCHAR: case PInvokeCore.WM_SYSDEADCHAR: if (!activeToolStrip.ContainsFocus) { // Route all keyboard messages to the active dropdown. m.HWnd = activeToolStrip.Handle; } break; } } return false; } internal static DPI_AWARENESS_CONTEXT GetDpiAwarenessContextForWindow(HWND hwnd) { DPI_AWARENESS_CONTEXT dpiAwarenessContext = DPI_AWARENESS_CONTEXT.UNSPECIFIED_DPI_AWARENESS_CONTEXT; if (OsVersion.IsWindows10_1607OrGreater()) { // Works only >= Windows 10/1607 DPI_AWARENESS_CONTEXT awarenessContext = PInvoke.GetWindowDpiAwarenessContext(hwnd); DPI_AWARENESS awareness = PInvoke.GetAwarenessFromDpiAwarenessContext(awarenessContext); dpiAwarenessContext = ConvertToDpiAwarenessContext(awareness); } return dpiAwarenessContext; } private static DPI_AWARENESS_CONTEXT ConvertToDpiAwarenessContext(DPI_AWARENESS dpiAwareness) => dpiAwareness switch { DPI_AWARENESS.DPI_AWARENESS_UNAWARE => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_UNAWARE, DPI_AWARENESS.DPI_AWARENESS_SYSTEM_AWARE => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE, DPI_AWARENESS.DPI_AWARENESS_PER_MONITOR_AWARE => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, _ => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE, }; } }