Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DATA_AND_PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
77 changes: 74 additions & 3 deletions src/modules/GrabAndMove/GrabAndMove/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand All @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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())
{
Expand Down
4 changes: 4 additions & 0 deletions src/settings-ui/Settings.UI.Library/GrabAndMoveProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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; }

Expand Down
Original file line number Diff line number Diff line change
@@ -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<GeneralSettings>.GetInstance(ISettingsUtilsMocks.GetStubSettingsUtils<GeneralSettings>().Object),
moduleSettings,
msg =>
{
serializedSettings = msg;
return 0;
});

viewModel.UseMiddleClickMaximize = false;

var outgoingSettings = JsonSerializer.Deserialize<SndModuleSettings<SndGrabAndMoveSettings>>(serializedSettings);
Assert.IsFalse(outgoingSettings!.PowertoysSetting.Settings.Properties.UseMiddleClickMaximize.Value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
<tkcontrols:SettingsCard x:Uid="GrabAndMove_ShowGeometry" HeaderIcon="{ui:FontIcon Glyph=&#xE809;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowGeometry, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="GrabAndMove_UseMiddleClickMaximize" HeaderIcon="{ui:FontIcon Glyph=&#xE740;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.UseMiddleClickMaximize, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>

<controls:SettingsGroup x:Uid="ExcludedApps" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
Expand Down
9 changes: 8 additions & 1 deletion src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -6162,6 +6162,13 @@ Text uses the current drawing color.</value>
<data name="GrabAndMove_UseAltResize.Description" xml:space="preserve">
<value>Hold the modifier key and drag with the right mouse button from the nearest edge or corner to resize the window</value>
</data>
<data name="GrabAndMove_UseMiddleClickMaximize.Header" xml:space="preserve">
<value>Maximize or restore windows with the modifier key + middle-click</value>
<comment>Middle-click refers to clicking the mouse wheel button.</comment>
</data>
<data name="GrabAndMove_UseMiddleClickMaximize.Description" xml:space="preserve">
<value>Hold the modifier key and middle-click inside a window to toggle between maximized and restored</value>
</data>
<data name="GrabAndMove_ExcludeApps.Header" xml:space="preserve">
<value>Excluded apps</value>
</data>
Expand All @@ -6172,4 +6179,4 @@ Text uses the current drawing color.</value>
<value>Example: outlook</value>
<comment>{Locked="outlook"}</comment>
</data>
</root>
</root>
15 changes: 15 additions & 0 deletions src/settings-ui/Settings.UI/ViewModels/GrabAndMoveViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading