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;