From 30ef65d5f8f73fb7f308b568cf2da1e10f056e84 Mon Sep 17 00:00:00 2001 From: Tim Wehren Date: Thu, 11 Jun 2026 10:38:49 +0200 Subject: [PATCH 1/2] Add Grab And Move middle-click maximize --- DATA_AND_PRIVACY.md | 2 +- src/modules/GrabAndMove/GrabAndMove/main.cpp | 77 ++++++++++++++++++- .../GrabAndMoveProperties.cs | 4 + .../ViewModelTests/GrabAndMove.cs | 45 +++++++++++ .../SettingsXAML/Views/GrabAndMovePage.xaml | 3 + .../Settings.UI/Strings/en-us/Resources.resw | 9 ++- .../ViewModels/GrabAndMoveViewModel.cs | 15 ++++ 7 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 src/settings-ui/Settings.UI.UnitTests/ViewModelTests/GrabAndMove.cs diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index e7afe680176c..3db4ddca6b8b 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -247,7 +247,7 @@ Thank you for using PowerToys! | Event Name | Description | | --- | --- | | Microsoft.PowerToys.GrabAndMove_EnableGrabAndMove | Triggered when Grab And Move is enabled or disabled. | -| Microsoft.PowerToys.GrabAndMove_ShortcutUse | Logs an attempt to move or resize a window via the Alt+Drag shortcut, including whether it succeeded, the action type (move or resize), and the reason (e.g., started, blocked by game mode). | +| Microsoft.PowerToys.GrabAndMove_ShortcutUse | Logs an attempt to move, resize, or maximize/restore a window via Grab And Move mouse shortcuts, including whether it succeeded, the action type (move, resize, or maximize), and the reason (e.g., started, toggled, blocked by game mode, or not maximizable). | ### Hosts File Editor diff --git a/src/modules/GrabAndMove/GrabAndMove/main.cpp b/src/modules/GrabAndMove/GrabAndMove/main.cpp index 505b8359dd17..ac2666f92cf3 100644 --- a/src/modules/GrabAndMove/GrabAndMove/main.cpp +++ b/src/modules/GrabAndMove/GrabAndMove/main.cpp @@ -58,7 +58,9 @@ static DWORD g_absorbedFlags = 0; // flags for replay (extended key, etc.) static bool g_showGeometry = false; // true if we want to draw the X, Y, W and H on the overlay on move and resize static bool g_doNotActivateOnGameMode = true; // true if GrabAndMove is suppressed when Windows Game Mode is active -static bool g_useAltResize = true; // This can be toggled from the settings. If false, Alt + right click does nothing. +static bool g_useAltResize = true; // This can be toggled from the settings. If false, Alt + right click does nothing. +static bool g_useMiddleClickMaximize = true; // If true, modifier + middle click toggles maximize/restore. +static bool g_middleClickConsumed = false; // Swallow the matching middle-button release after a handled press. // Count of non-modifier keys currently held. Used to suppress GrabAndMove when the // modifier key is pressed while another key is already down (e.g. Q held, then modifier pressed). @@ -225,11 +227,23 @@ enum class GrabAndMoveShortcutAction { Move, Resize, + Maximize, }; static void TraceShortcutUse(bool successful, GrabAndMoveShortcutAction action, const wchar_t* reason) noexcept { - const wchar_t* actionName = action == GrabAndMoveShortcutAction::Move ? L"move" : L"resize"; + const wchar_t* actionName = L"move"; + switch (action) + { + case GrabAndMoveShortcutAction::Resize: + actionName = L"resize"; + break; + case GrabAndMoveShortcutAction::Maximize: + actionName = L"maximize"; + break; + default: + break; + } TraceLoggingWrite( g_hProvider, @@ -241,6 +255,22 @@ static void TraceShortcutUse(bool successful, GrabAndMoveShortcutAction action, TraceLoggingWideString(reason, "Reason")); } +static bool IsWindowMaximizable(HWND hwnd) +{ + return (GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_MAXIMIZEBOX) != 0; +} + +static bool ToggleWindowMaximized(HWND hwnd) +{ + if (!hwnd || !IsWindowMaximizable(hwnd)) + { + return false; + } + + ShowWindow(hwnd, IsZoomed(hwnd) ? SW_RESTORE : SW_MAXIMIZE); + return true; +} + // --------------------------------------------------------------------------- // Settings file helpers // --------------------------------------------------------------------------- @@ -270,6 +300,11 @@ static void LoadSettingsFromFile() g_useAltResize = *v; } + if (auto v = values.get_bool_value(L"useMiddleClickMaximize")) + { + g_useMiddleClickMaximize = *v; + } + if (auto v = values.get_int_value(L"modifierKey")) { g_modifierKey = (*v == 1) ? GrabAndMoveModifier::Win : GrabAndMoveModifier::Alt; @@ -1011,16 +1046,52 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) goto forward; // Recovery path: if a non-modifier click occurs while stale drag/resize state exists, clear it. - if ((wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN) && !IsActivationModifierPressed()) + if ((wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN) && !IsActivationModifierPressed()) { EndInteraction(true, true); } + if (wParam == WM_MBUTTONUP && g_middleClickConsumed) + { + g_middleClickConsumed = false; + return 1; + } + if (!g_dragging && !g_resizing && g_hOverlay && IsWindowVisible(g_hOverlay)) { HideOverlay(); } + // ----- Alt + Middle Button Down: toggle maximize/restore ----- + if (wParam == WM_MBUTTONDOWN && g_useMiddleClickMaximize && IsActivationModifierPressed()) + { + if (IsSuppressedByGameMode()) + { + TraceShortcutUse(false, GrabAndMoveShortcutAction::Maximize, L"game_mode"); + goto forward; + } + + POINT pt = ms->pt; + HWND hwnd = ResolveTargetWindow(pt); + if (hwnd) + { + if (IsExcluded(hwnd)) + { + goto forward; + } + + if (ToggleWindowMaximized(hwnd)) + { + g_dragConsumedAlt = true; + g_middleClickConsumed = true; + TraceShortcutUse(true, GrabAndMoveShortcutAction::Maximize, L"toggled"); + return 1; + } + + TraceShortcutUse(false, GrabAndMoveShortcutAction::Maximize, L"not_maximizable"); + } + } + // ----- Alt + Left Button Down: start drag ----- if (wParam == WM_LBUTTONDOWN && IsActivationModifierPressed()) { diff --git a/src/settings-ui/Settings.UI.Library/GrabAndMoveProperties.cs b/src/settings-ui/Settings.UI.Library/GrabAndMoveProperties.cs index f022c595d0c0..32a322c28a20 100644 --- a/src/settings-ui/Settings.UI.Library/GrabAndMoveProperties.cs +++ b/src/settings-ui/Settings.UI.Library/GrabAndMoveProperties.cs @@ -14,6 +14,7 @@ public GrabAndMoveProperties() DoNotActivateOnGameMode = new BoolProperty(true); ShowGeometry = new BoolProperty(false); UseAltResize = new BoolProperty(true); + UseMiddleClickMaximize = new BoolProperty(true); ExcludedApps = new StringProperty(); ModifierKey = new IntProperty(0); // 0 = Alt, 1 = Win } @@ -30,6 +31,9 @@ public GrabAndMoveProperties() [JsonPropertyName("useAltResize")] public BoolProperty UseAltResize { get; set; } + [JsonPropertyName("useMiddleClickMaximize")] + public BoolProperty UseMiddleClickMaximize { get; set; } + [JsonPropertyName("doNotActivateOnGameMode")] public BoolProperty DoNotActivateOnGameMode { get; set; } diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/GrabAndMove.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/GrabAndMove.cs new file mode 100644 index 000000000000..86b217fe38d2 --- /dev/null +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/GrabAndMove.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.UnitTests.Mocks; +using Microsoft.PowerToys.Settings.UI.ViewModels; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ViewModelTests +{ + [TestClass] + public class GrabAndMove + { + [TestMethod] + public void MiddleClickMaximizeIsEnabledByDefault() + { + var settings = new GrabAndMoveSettings(); + + Assert.IsTrue(settings.Properties.UseMiddleClickMaximize.Value); + } + + [TestMethod] + public void UpdatingMiddleClickMaximizeNotifiesRunner() + { + var moduleSettings = new GrabAndMoveSettings(); + string serializedSettings = string.Empty; + + using var viewModel = new GrabAndMoveViewModel( + SettingsRepository.GetInstance(ISettingsUtilsMocks.GetStubSettingsUtils().Object), + moduleSettings, + msg => + { + serializedSettings = msg; + return 0; + }); + + viewModel.UseMiddleClickMaximize = false; + + var outgoingSettings = JsonSerializer.Deserialize>(serializedSettings); + Assert.IsFalse(outgoingSettings!.PowertoysSetting.Settings.Properties.UseMiddleClickMaximize.Value); + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GrabAndMovePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GrabAndMovePage.xaml index 4e6ea76cf28d..0270ecf6ed14 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GrabAndMovePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GrabAndMovePage.xaml @@ -59,6 +59,9 @@ + + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 1e4bc972bc11..e8fca2391736 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -6162,6 +6162,13 @@ Text uses the current drawing color. Hold the modifier key and drag with the right mouse button from the nearest edge or corner to resize the window + + Maximize or restore windows with the modifier key + middle-click + Middle-click refers to clicking the mouse wheel button. + + + Hold the modifier key and middle-click inside a window to toggle between maximized and restored + Excluded apps @@ -6172,4 +6179,4 @@ Text uses the current drawing color. Example: outlook {Locked="outlook"} - \ No newline at end of file + diff --git a/src/settings-ui/Settings.UI/ViewModels/GrabAndMoveViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GrabAndMoveViewModel.cs index 114114a99714..a8a70df78da7 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GrabAndMoveViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GrabAndMoveViewModel.cs @@ -147,6 +147,21 @@ public bool UseAltResize } } + public bool UseMiddleClickMaximize + { + get => _moduleSettings.Properties.UseMiddleClickMaximize.Value; + + set + { + if (_moduleSettings.Properties.UseMiddleClickMaximize.Value != value) + { + _moduleSettings.Properties.UseMiddleClickMaximize.Value = value; + NotifyModuleSettingsChanged(); + OnPropertyChanged(nameof(UseMiddleClickMaximize)); + } + } + } + public bool DoNotActivateOnGameMode { get => _moduleSettings.Properties.DoNotActivateOnGameMode.Value; From d3c0c31f144e7cd65aa5ef44597b092dd74caa7e Mon Sep 17 00:00:00 2001 From: Tim Wehren Date: Thu, 11 Jun 2026 12:49:20 +0200 Subject: [PATCH 2/2] Fix Grab And Move spelling metadata --- DATA_AND_PRIVACY.md | 2 +- src/modules/GrabAndMove/GrabAndMove/main.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index 3db4ddca6b8b..c877be1d231b 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -247,7 +247,7 @@ Thank you for using PowerToys! | Event Name | Description | | --- | --- | | Microsoft.PowerToys.GrabAndMove_EnableGrabAndMove | Triggered when Grab And Move is enabled or disabled. | -| Microsoft.PowerToys.GrabAndMove_ShortcutUse | Logs an attempt to move, resize, or maximize/restore a window via Grab And Move mouse shortcuts, including whether it succeeded, the action type (move, resize, or maximize), and the reason (e.g., started, toggled, blocked by game mode, or not maximizable). | +| Microsoft.PowerToys.GrabAndMove_ShortcutUse | Logs an attempt to move, resize, or maximize/restore a window via Grab And Move mouse shortcuts, including whether it succeeded, the action type (move, resize, or maximize), and the reason (e.g., started, toggled, blocked by game mode, or missing a maximize box). | ### Hosts File Editor diff --git a/src/modules/GrabAndMove/GrabAndMove/main.cpp b/src/modules/GrabAndMove/GrabAndMove/main.cpp index ac2666f92cf3..15bd0e0abcf0 100644 --- a/src/modules/GrabAndMove/GrabAndMove/main.cpp +++ b/src/modules/GrabAndMove/GrabAndMove/main.cpp @@ -255,14 +255,14 @@ static void TraceShortcutUse(bool successful, GrabAndMoveShortcutAction action, TraceLoggingWideString(reason, "Reason")); } -static bool IsWindowMaximizable(HWND hwnd) +static bool CanWindowMaximize(HWND hwnd) { return (GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_MAXIMIZEBOX) != 0; } static bool ToggleWindowMaximized(HWND hwnd) { - if (!hwnd || !IsWindowMaximizable(hwnd)) + if (!hwnd || !CanWindowMaximize(hwnd)) { return false; } @@ -1088,7 +1088,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) return 1; } - TraceShortcutUse(false, GrabAndMoveShortcutAction::Maximize, L"not_maximizable"); + TraceShortcutUse(false, GrabAndMoveShortcutAction::Maximize, L"missing_maximize_box"); } }