diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index e7afe680176c..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 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 missing a maximize box). | ### Hosts File Editor diff --git a/src/modules/GrabAndMove/GrabAndMove/main.cpp b/src/modules/GrabAndMove/GrabAndMove/main.cpp index 505b8359dd17..15bd0e0abcf0 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 CanWindowMaximize(HWND hwnd) +{ + return (GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_MAXIMIZEBOX) != 0; +} + +static bool ToggleWindowMaximized(HWND hwnd) +{ + if (!hwnd || !CanWindowMaximize(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"missing_maximize_box"); + } + } + // ----- 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;