diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.cs b/src/modules/MouseWithoutBorders/App/Class/Common.cs
index 8d4ff7f326..0494a952fd 100644
--- a/src/modules/MouseWithoutBorders/App/Class/Common.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/Common.cs
@@ -13,6 +13,7 @@ using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@@ -1560,5 +1561,98 @@ namespace MouseWithoutBorders
}
}
}
+
+ private static bool DisableEasyMouseWhenForegroundWindowIsFullscreenSetting()
+ {
+ return Setting.Values.DisableEasyMouseWhenForegroundWindowIsFullscreen;
+ }
+
+ private static bool IsAppIgnoredByEasyMouseFullscreenCheck(IntPtr foregroundWindowHandle)
+ {
+ if (NativeMethods.GetWindowThreadProcessId(foregroundWindowHandle, out var processId) == 0)
+ {
+ Logger.LogDebug($"GetWindowThreadProcessId failed with error : {Marshal.GetLastWin32Error()}");
+ return false;
+ }
+
+ var processHandle = NativeMethods.OpenProcess(0x1000, false, processId);
+ if (processHandle == IntPtr.Zero)
+ {
+ return false;
+ }
+
+ uint maxPath = 260;
+ var nameBuffer = new char[maxPath];
+ if (!NativeMethods.QueryFullProcessImageName(
+ processHandle, NativeMethods.QUERY_FULL_PROCESS_NAME_FLAGS.DEFAULT, nameBuffer, ref maxPath))
+ {
+ Logger.LogDebug($"QueryFullProcessImageName failed with error : {Marshal.GetLastWin32Error()}");
+ NativeMethods.CloseHandle(processHandle);
+ return false;
+ }
+
+ NativeMethods.CloseHandle(processHandle);
+
+ var name = new string(nameBuffer, 0, (int)maxPath);
+
+ var excludedApps = Setting.Values.EasyMouseFullscreenSwitchBlockExcludedApps;
+
+ return excludedApps.Contains(Path.GetFileNameWithoutExtension(name), StringComparer.OrdinalIgnoreCase)
+ || excludedApps.Contains(Path.GetFileName(name), StringComparer.OrdinalIgnoreCase);
+ }
+
+ internal static bool IsEasyMouseBlockedByFullscreenWindow()
+ {
+ var shellHandle = NativeMethods.GetShellWindow();
+ var desktopHandle = NativeMethods.GetDesktopWindow();
+ var foregroundHandle = NativeMethods.GetForegroundWindow();
+
+ // If the foreground window is either the desktop or the Windows shell, we are not in fullscreen mode.
+ if (foregroundHandle.Equals(shellHandle) || foregroundHandle.Equals(desktopHandle))
+ {
+ return false;
+ }
+
+ if (NativeMethods.SHQueryUserNotificationState(out var userNotificationState) != 0)
+ {
+ Logger.LogDebug($"SHQueryUserNotificationState failed with error : {Marshal.GetLastWin32Error()}");
+ return false;
+ }
+
+ switch (userNotificationState)
+ {
+ // An application running in full screen mode, check if the foreground window is
+ // listed as ignored in the settings.
+ case NativeMethods.USER_NOTIFICATION_STATE.BUSY:
+ case NativeMethods.USER_NOTIFICATION_STATE.RUNNING_D3D_FULL_SCREEN:
+ case NativeMethods.USER_NOTIFICATION_STATE.PRESENTATION_MODE:
+ return !IsAppIgnoredByEasyMouseFullscreenCheck(foregroundHandle);
+
+ // No full screen app running.
+ case NativeMethods.USER_NOTIFICATION_STATE.NOT_PRESENT:
+ case NativeMethods.USER_NOTIFICATION_STATE.ACCEPTS_NOTIFICATIONS:
+ case NativeMethods.USER_NOTIFICATION_STATE.QUIET_TIME:
+ // Cannot determine
+ case NativeMethods.USER_NOTIFICATION_STATE.APP:
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Check if a machine switch triggered by EasyMouse would be allowed to proceed due to other settings.
+ ///
+ /// A boolean that tells us if the switch isn't blocked by any other settings
+ internal static bool IsEasyMouseSwitchAllowed()
+ {
+ // Never prevent a switch if we are not moving out of the host machine.
+ if (!DisableEasyMouseWhenForegroundWindowIsFullscreenSetting() || DesMachineID != MachineID)
+ {
+ return true;
+ }
+
+ // Check if the switch is blocked by a full-screen window running in the foreground
+ return !IsEasyMouseBlockedByFullscreenWindow();
+ }
}
}
diff --git a/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs b/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs
index 1b045a61a0..539e0267bd 100644
--- a/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs
@@ -122,9 +122,16 @@ namespace MouseWithoutBorders.Class
[DllImport("user32.dll", SetLastError = false)]
internal static extern IntPtr GetDesktopWindow();
+ [LibraryImport("user32.dll")]
+ internal static partial IntPtr GetShellWindow();
+
[DllImport("user32.dll")]
internal static extern IntPtr GetWindowDC(IntPtr hWnd);
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool GetWindowRect(IntPtr hWnd, out RECT rect);
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int DrawText(IntPtr hDC, string lpString, int nCount, ref RECT lpRect, uint uFormat);
@@ -291,6 +298,17 @@ namespace MouseWithoutBorders.Class
[DllImport("user32.dll", SetLastError = true)]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
+ [LibraryImport("kernel32.dll",
+ EntryPoint = "QueryFullProcessImageNameW",
+ SetLastError = true,
+ StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool QueryFullProcessImageName(
+ IntPtr hProcess, QUERY_FULL_PROCESS_NAME_FLAGS dwFlags, [Out] char[] lpExeName, ref uint lpdwSize);
+
+ [LibraryImport("shell32.dll", SetLastError = true)]
+ internal static partial int SHQueryUserNotificationState(out USER_NOTIFICATION_STATE state);
+
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
@@ -333,11 +351,11 @@ namespace MouseWithoutBorders.Class
[DllImport("ntdll.dll")]
internal static extern int NtQueryInformationProcess(
- IntPtr hProcess,
- int processInformationClass /* 0 */,
- ref PROCESS_BASIC_INFORMATION processBasicInformation,
- uint processInformationLength,
- out uint returnLength);
+ IntPtr hProcess,
+ int processInformationClass /* 0 */,
+ ref PROCESS_BASIC_INFORMATION processBasicInformation,
+ uint processInformationLength,
+ out uint returnLength);
#endif
#if USE_GetSecurityDescriptorSacl
@@ -632,14 +650,14 @@ namespace MouseWithoutBorders.Class
{
internal int LowPart;
internal int HighPart;
- }// end struct
+ } // end struct
[StructLayout(LayoutKind.Sequential)]
internal struct LUID_AND_ATTRIBUTES
{
internal LUID Luid;
internal int Attributes;
- }// end struct
+ } // end struct
[StructLayout(LayoutKind.Sequential)]
internal struct TOKEN_PRIVILEGES
@@ -670,23 +688,23 @@ namespace MouseWithoutBorders.Class
internal const int TOKEN_ADJUST_SESSIONID = 0x0100;
internal const int TOKEN_ALL_ACCESS_P = STANDARD_RIGHTS_REQUIRED |
- TOKEN_ASSIGN_PRIMARY |
- TOKEN_DUPLICATE |
- TOKEN_IMPERSONATE |
- TOKEN_QUERY |
- TOKEN_QUERY_SOURCE |
- TOKEN_ADJUST_PRIVILEGES |
- TOKEN_ADJUST_GROUPS |
- TOKEN_ADJUST_DEFAULT;
+ TOKEN_ASSIGN_PRIMARY |
+ TOKEN_DUPLICATE |
+ TOKEN_IMPERSONATE |
+ TOKEN_QUERY |
+ TOKEN_QUERY_SOURCE |
+ TOKEN_ADJUST_PRIVILEGES |
+ TOKEN_ADJUST_GROUPS |
+ TOKEN_ADJUST_DEFAULT;
internal const int TOKEN_ALL_ACCESS = TOKEN_ALL_ACCESS_P | TOKEN_ADJUST_SESSIONID;
internal const int TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY;
internal const int TOKEN_WRITE = STANDARD_RIGHTS_WRITE |
- TOKEN_ADJUST_PRIVILEGES |
- TOKEN_ADJUST_GROUPS |
- TOKEN_ADJUST_DEFAULT;
+ TOKEN_ADJUST_PRIVILEGES |
+ TOKEN_ADJUST_GROUPS |
+ TOKEN_ADJUST_DEFAULT;
internal const int TOKEN_EXECUTE = STANDARD_RIGHTS_EXECUTE;
@@ -940,6 +958,30 @@ namespace MouseWithoutBorders.Class
NameDnsDomain = 12,
}
+ internal enum MONITOR_FROM_WINDOW_FLAGS : uint
+ {
+ DEFAULT_TO_NULL = 0x00000000,
+ DEFAULT_TO_PRIMARY = 0x00000001,
+ DEFAULT_TO_NEAREST = 0x00000002,
+ }
+
+ internal enum QUERY_FULL_PROCESS_NAME_FLAGS : uint
+ {
+ DEFAULT = 0x00000000,
+ PROCESS_NAME_NATIVE = 0x00000001,
+ }
+
+ internal enum USER_NOTIFICATION_STATE
+ {
+ NOT_PRESENT = 1,
+ BUSY = 2,
+ RUNNING_D3D_FULL_SCREEN = 3,
+ PRESENTATION_MODE = 4,
+ ACCEPTS_NOTIFICATIONS = 5,
+ QUIET_TIME = 6,
+ APP = 7,
+ }
+
[DllImport("secur32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.I1)]
internal static extern bool GetUserNameEx(int nameFormat, StringBuilder userName, ref uint userNameSize);
diff --git a/src/modules/MouseWithoutBorders/App/Class/Setting.cs b/src/modules/MouseWithoutBorders/App/Class/Setting.cs
index 72365fe478..30b99a97d0 100644
--- a/src/modules/MouseWithoutBorders/App/Class/Setting.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/Setting.cs
@@ -414,6 +414,44 @@ namespace MouseWithoutBorders.Class
}
}
+ internal bool DisableEasyMouseWhenForegroundWindowIsFullscreen
+ {
+ get
+ {
+ lock (_loadingSettingsLock)
+ {
+ return _properties.DisableEasyMouseWhenForegroundWindowIsFullscreen;
+ }
+ }
+
+ set
+ {
+ lock (_loadingSettingsLock)
+ {
+ _properties.DisableEasyMouseWhenForegroundWindowIsFullscreen = value;
+ }
+ }
+ }
+
+ internal HashSet EasyMouseFullscreenSwitchBlockExcludedApps
+ {
+ get
+ {
+ lock (_loadingSettingsLock)
+ {
+ return _properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value;
+ }
+ }
+
+ set
+ {
+ lock (_loadingSettingsLock)
+ {
+ _properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value = value;
+ }
+ }
+ }
+
internal string Enc(string st, bool dec, DataProtectionScope protectionScope)
{
if (st == null || st.Length < 1)
diff --git a/src/modules/MouseWithoutBorders/App/Core/Event.cs b/src/modules/MouseWithoutBorders/App/Core/Event.cs
index 7856a64d87..c30c59e547 100644
--- a/src/modules/MouseWithoutBorders/App/Core/Event.cs
+++ b/src/modules/MouseWithoutBorders/App/Core/Event.cs
@@ -66,13 +66,17 @@ internal static class Event
try
{
Common.PaintCount = 0;
- bool switchByMouseEnabled = IsSwitchingByMouseEnabled();
- if (switchByMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == Common.WM_MOUSEMOVE)
+ // Check if easy mouse setting is enabled.
+ bool isEasyMouseEnabled = IsSwitchingByMouseEnabled();
+
+ if (isEasyMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == Common.WM_MOUSEMOVE)
{
Point p = MachineStuff.MoveToMyNeighbourIfNeeded(e.X, e.Y, MachineStuff.desMachineID);
- if (!p.IsEmpty)
+ // Check if easy mouse switches are disabled when an application is running in fullscreen mode,
+ // if they are, check that there is no application running in fullscreen mode before switching.
+ if (!p.IsEmpty && Common.IsEasyMouseSwitchAllowed())
{
Common.HasSwitchedMachineSinceLastCopy = true;
@@ -165,7 +169,8 @@ internal static class Event
string newDesMachineName = MachineStuff.NameFromID(newDesMachineID);
if (!Common.IsConnectedTo(newDesMachineID))
- {// Connection lost, cancel switching
+ {
+ // Connection lost, cancel switching
Logger.LogDebug("No active connection found for " + newDesMachineName);
// ShowToolTip("No active connection found for [" + newDesMachineName + "]!", 500);
diff --git a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs
index 265b8a1e2d..e234ee6b2f 100644
--- a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs
@@ -92,6 +92,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public IntProperty EasyMouse { get; set; }
+ [JsonConverter(typeof(BoolPropertyJsonConverter))]
+ public bool DisableEasyMouseWhenForegroundWindowIsFullscreen { get; set; }
+
+ // Apps that are to be excluded when using DisableEasyMouseWhenForegroundWindowIsFullscreen
+ // meaning that it is possible to switch screen when these apps are running in fullscreen.
+ [CmdConfigureIgnore]
+ public GenericProperty> EasyMouseFullscreenSwitchBlockExcludedApps { get; set; }
+
[CmdConfigureIgnore]
public IntProperty MachineID { get; set; }
@@ -173,6 +181,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
ShowOriginalUI = false;
UseService = false;
+ DisableEasyMouseWhenForegroundWindowIsFullscreen = true;
+ EasyMouseFullscreenSwitchBlockExcludedApps = new GenericProperty>(new HashSet(StringComparer.OrdinalIgnoreCase));
+
HotKeySwitchMachine = new IntProperty(0x70); // VK.F1
ToggleEasyMouseShortcut = DefaultHotKeyToggleEasyMouse;
LockMachineShortcut = DefaultHotKeyLockMachine;
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml
index 32f4e79d8b..c95bb47dee 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml
@@ -267,7 +267,8 @@
-
+
+
@@ -276,7 +277,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Advanced Settings
+
+ Easy Mouse
+
Easy Mouse: move between machines by moving the mouse pointer to the screen edges.
@@ -419,6 +422,27 @@
Shift
This is the Shift keyboard key
+
+ Disable Easy Mouse when an application is running in full screen.
+
+
+ Prevent Easy Mouse from moving to another machine when an application is in full-screen mode.
+
+
+ Disabling Easy Mouse in full-screen mode only affects the host PC. It won’t stop the mouse from moving away from remote machines.
+
+
+ msedge.exe
+firefox.exe
+opera.exe
+ Allow easy mouse when chrome is in fullscreen mode.
+
+
+ Ignored fullscreen applications
+
+
+ Allow Easy Mouse to move between machines even if one of these applications is running in full screen, separate each executable with a new line.
+
Shortcut to lock all machines.
diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs
index 3d81acd81d..6e0bcaa444 100644
--- a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs
@@ -904,6 +904,43 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ private string _easyMouseIgnoredFullscreenAppsString;
+
+ public string EasyMouseFullscreenSwitchBlockExcludedApps
+ {
+ // Convert the list of excluded apps retrieved from the settings
+ // to a single string that can be displayed in the bound textbox
+ get
+ {
+ if (_easyMouseIgnoredFullscreenAppsString == null)
+ {
+ var excludedApps = Settings.Properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value;
+ _easyMouseIgnoredFullscreenAppsString = excludedApps.Count == 0 ? string.Empty : string.Join('\r', excludedApps);
+ }
+
+ return _easyMouseIgnoredFullscreenAppsString;
+ }
+
+ set
+ {
+ if (EasyMouseFullscreenSwitchBlockExcludedApps == value)
+ {
+ return;
+ }
+
+ _easyMouseIgnoredFullscreenAppsString = value;
+
+ var ignoredAppsSet = new HashSet(StringComparer.OrdinalIgnoreCase);
+ if (value != string.Empty)
+ {
+ ignoredAppsSet.UnionWith(value.Split('\r', StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ Settings.Properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value = ignoredAppsSet;
+ NotifyPropertyChanged();
+ }
+ }
+
public bool CardForName2IpSettingIsEnabled => _disableUserDefinedIpMappingRulesIsGPOConfigured == false;
public bool ShowPolicyConfiguredInfoForName2IPSetting => _disableUserDefinedIpMappingRulesIsGPOConfigured && IsEnabled;
@@ -1005,6 +1042,30 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public bool EasyMouseEnabled => (EasyMouseOption)EasyMouseOptionIndex != EasyMouseOption.Disable;
+
+ public bool IsEasyMouseBlockingOnFullscreenEnabled =>
+ EasyMouseEnabled && DisableEasyMouseWhenForegroundWindowIsFullscreen;
+
+ public bool DisableEasyMouseWhenForegroundWindowIsFullscreen
+ {
+ get
+ {
+ return Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen;
+ }
+
+ set
+ {
+ if (Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen == value)
+ {
+ return;
+ }
+
+ Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen = value;
+ NotifyPropertyChanged();
+ }
+ }
+
public HotkeySettings ToggleEasyMouseShortcut
{
get => Settings.Properties.ToggleEasyMouseShortcut;