diff --git a/PowerToys.sln b/PowerToys.sln index 043585c7ed..46272018cc 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -830,8 +830,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewMod EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunnerV2", "src\RunnerV2\RunnerV2\RunnerV2.csproj", "{20C43796-E14D-47B2-843A-843CAC9C0D28}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlwaysOnTopModuleInterface", "src\modules\alwaysontop\AlwaysOnTopModuleInterface\AlwaysOnTopModuleInterface.csproj", "{2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -3018,14 +3016,6 @@ Global {20C43796-E14D-47B2-843A-843CAC9C0D28}.Release|ARM64.Build.0 = Release|ARM64 {20C43796-E14D-47B2-843A-843CAC9C0D28}.Release|x64.ActiveCfg = Release|x64 {20C43796-E14D-47B2-843A-843CAC9C0D28}.Release|x64.Build.0 = Release|x64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Debug|ARM64.Build.0 = Debug|ARM64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Debug|x64.ActiveCfg = Debug|x64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Debug|x64.Build.0 = Debug|x64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Release|ARM64.ActiveCfg = Release|ARM64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Release|ARM64.Build.0 = Release|ARM64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Release|x64.ActiveCfg = Release|x64 - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3354,7 +3344,6 @@ Global {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482} {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {2CF11A08-1A1F-4F75-BC41-F982FCF26D2F} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/RunnerV2/RunnerV2/Helpers/HotkeyManager.cs b/src/RunnerV2/RunnerV2/Helpers/HotkeyManager.cs index 3cdaf8c6b5..cdf87db822 100644 --- a/src/RunnerV2/RunnerV2/Helpers/HotkeyManager.cs +++ b/src/RunnerV2/RunnerV2/Helpers/HotkeyManager.cs @@ -16,19 +16,19 @@ namespace RunnerV2.Helpers { internal static partial class HotkeyManager { - private static readonly Dictionary _hotkeyActions = []; + private static readonly Dictionary _hotkeyActions = []; [STAThread] public static void EnableHotkey(HotkeyEx hotkey, Action onHotkey) { - if (_hotkeyActions.ContainsKey(hotkey)) + if (_hotkeyActions.ContainsKey(hotkey.Identifier)) { - return; + DisableHotkey(hotkey); } - _hotkeyActions[hotkey] = onHotkey; + _hotkeyActions[hotkey.Identifier] = onHotkey; - if (!RegisterHotKey(Runner.RunnerHwnd, hotkey.GetHashCode(), hotkey.ModifiersMask, hotkey.VkCode)) + if (!RegisterHotKey(Runner.RunnerHwnd, hotkey.Identifier, hotkey.ModifiersMask, hotkey.VkCode)) { Console.WriteLine("Failed to register hotkey: " + hotkey); var lastError = Marshal.GetLastWin32Error(); @@ -36,23 +36,29 @@ namespace RunnerV2.Helpers } } + [STAThread] public static void DisableHotkey(HotkeyEx hotkey) { - if (!_hotkeyActions.ContainsKey(hotkey)) + if (!_hotkeyActions.ContainsKey(hotkey.Identifier)) { return; } - _hotkeyActions.Remove(hotkey); - UnregisterHotKey(IntPtr.Zero, hotkey.GetHashCode()); + _hotkeyActions.Remove(hotkey.Identifier); + if (!UnregisterHotKey(Runner.RunnerHwnd, hotkey.Identifier)) + { + Console.WriteLine("Failed to unregister hotkey: " + hotkey); + var lastError = Marshal.GetLastWin32Error(); + Console.WriteLine("LastError: " + lastError); + } } public static void ProcessHotkey(nuint hotkeyId) { ulong hashId = hotkeyId.ToUInt64(); - if (_hotkeyActions.Any(h => h.Key.GetHashCode() == (int)hashId)) + if (_hotkeyActions.Any(h => h.Key == (int)hashId)) { - _hotkeyActions.First(h => h.Key.GetHashCode() == (int)hashId).Value(); + _hotkeyActions.First(h => h.Key == (int)hashId).Value(); } } } diff --git a/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs b/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs index 6ed166068b..a9d288bc61 100644 --- a/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs +++ b/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs @@ -13,6 +13,7 @@ using System.Text.Json; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using PowerToys.Interop; +using Windows.Media.Devices; namespace RunnerV2.Helpers { @@ -113,16 +114,32 @@ namespace RunnerV2.Helpers switch (property.Name) { case "action": - _settingsUtils.SaveSettings(property.Value.GetProperty("general").ToString(), string.Empty); - switch (property.Value.GetProperty("general").GetProperty("action_name").GetString()) + foreach (var moduleName in property.Value.EnumerateObject()) { - case "restart_elevation": - ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevatedWithOpenSettings; - Runner.Close(); - break; - case "request_update_state_date": - // Todo: + _settingsUtils.SaveSettings(moduleName.Value.ToString(), moduleName.Name); + if (moduleName.Name == "general") + { + switch (moduleName.Value.GetProperty("action_name").GetString()) + { + case "restart_elevation": + ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevatedWithOpenSettings; + Runner.Close(); + break; + case "request_update_state_date": + // Todo: + break; + } + break; + } + + foreach (IPowerToysModule ptModule in Runner.LoadedModules) + { + if (ptModule.CustomActions.TryGetValue(moduleName.Value.GetProperty("action_name").GetString() ?? string.Empty, out Action? action)) + { + action(); + } + } } break; @@ -140,10 +157,11 @@ namespace RunnerV2.Helpers break; case "general": _settingsUtils.SaveSettings(property.Value.ToString(), string.Empty); - foreach (IPowerToysModule module in Runner.LoadedModules) + NativeMethods.PostMessageW(Runner.RunnerHwnd, (uint)NativeMethods.WindowMessages.REFRESH_SETTINGS, 0, 0); + + foreach (IPowerToysModule module in Runner.ModulesToLoad) { module.OnSettingsChanged("general", property.Value); - Runner.ToggleModuleStateBasedOnEnabledProperty(module); } break; @@ -164,6 +182,8 @@ namespace RunnerV2.Helpers module2.OnSettingsChanged(powertoysSettingsPart.Name, powertoysSettingsPart.Value); } } + + NativeMethods.PostMessageW(Runner.RunnerHwnd, (uint)NativeMethods.WindowMessages.REFRESH_SETTINGS, 0, 0); } break; diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs b/src/RunnerV2/RunnerV2/ModuleInterfaces/AlwaysOnTopModuleInterface.cs similarity index 60% rename from src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs rename to src/RunnerV2/RunnerV2/ModuleInterfaces/AlwaysOnTopModuleInterface.cs index c582144f61..8501efb299 100644 --- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs +++ b/src/RunnerV2/RunnerV2/ModuleInterfaces/AlwaysOnTopModuleInterface.cs @@ -3,17 +3,20 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Runtime.InteropServices; +using System.Text.Json; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using PowerToys.GPOWrapper; -namespace AlwaysOnTopModuleInterface +namespace RunnerV2.ModuleInterfaces { - public partial class ModuleInterface : IPowerToysModule, IDisposable + public partial class AlwaysOnTopModuleInterface : IPowerToysModule, IDisposable { + private static readonly ushort _pinHotkeyAtom = NativeMethods.AddAtomW("PowerToys_AlwaysOnTop_PinHotkey"); + public bool Enabled => new SettingsUtils().GetSettings().Enabled.AlwaysOnTop; public string Name => "AlwaysOnTop"; @@ -29,13 +32,17 @@ namespace AlwaysOnTopModuleInterface InteropEvent terminateEventWrapper = new(InteropEvent.AlwaysOnTopTerminate); terminateEventWrapper.Fire(); terminateEventWrapper.Dispose(); - _process?.Dispose(); _pinEventWrapper?.Dispose(); _pinEventWrapper = null; } public void Enable() { + if (_process?.HasExited == false) + { + return; + } + _pinEventWrapper = new InteropEvent(InteropEvent.AlwaysOnTopPin); var psi = new ProcessStartInfo @@ -48,15 +55,29 @@ namespace AlwaysOnTopModuleInterface _process = Process.Start(psi); } - public HotkeyEx HotkeyEx => new SettingsUtils().GetSettings(Name).Properties.Hotkey.Value; - - public Action OnHotkey => () => + public AlwaysOnTopModuleInterface() { - if (!_process?.HasExited ?? false) + InitializeHotkey(); + } + + private void InitializeHotkey() + { + Hotkeys.Clear(); + Hotkeys.Add(((HotkeyEx)new SettingsUtils().GetSettings(Name).Properties.Hotkey.Value) with { Identifier = 0x1 }, () => { - _pinEventWrapper?.Fire(); - } - }; + if (!_process?.HasExited ?? false) + { + _pinEventWrapper?.Fire(); + } + }); + } + + public void OnSettingsChanged(string settingsKind, JsonElement jsonProperties) + { + InitializeHotkey(); + } + + public Dictionary Hotkeys { get; } = []; public void Dispose() { diff --git a/src/modules/Hosts/Hosts/ModuleInterface.cs b/src/RunnerV2/RunnerV2/ModuleInterfaces/HostsModuleInterface.cs similarity index 87% rename from src/modules/Hosts/Hosts/ModuleInterface.cs rename to src/RunnerV2/RunnerV2/ModuleInterfaces/HostsModuleInterface.cs index 6360762ad2..ae69334e6d 100644 --- a/src/modules/Hosts/Hosts/ModuleInterface.cs +++ b/src/RunnerV2/RunnerV2/ModuleInterfaces/HostsModuleInterface.cs @@ -3,15 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; -using System.IO; -using System.Reflection; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using PowerToys.GPOWrapper; -namespace Hosts +namespace RunnerV2.ModuleInterfaces { - internal sealed class ModuleInterface : IPowerToysModule + internal sealed class HostsModuleInterface : IPowerToysModule { public bool Enabled => new SettingsUtils().GetSettingsOrDefault().Enabled.Hosts; diff --git a/src/modules/poweraccent/PowerAccent.Core/ModuleInterface.cs b/src/RunnerV2/RunnerV2/ModuleInterfaces/PowerAccentModuleInterface.cs similarity index 81% rename from src/modules/poweraccent/PowerAccent.Core/ModuleInterface.cs rename to src/RunnerV2/RunnerV2/ModuleInterfaces/PowerAccentModuleInterface.cs index 09f693125b..6b1722ecac 100644 --- a/src/modules/poweraccent/PowerAccent.Core/ModuleInterface.cs +++ b/src/RunnerV2/RunnerV2/ModuleInterfaces/PowerAccentModuleInterface.cs @@ -2,15 +2,20 @@ // 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; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using PowerToys.GPOWrapper; -namespace PowerAccent.Core +namespace RunnerV2.ModuleInterfaces { - internal sealed class ModuleInterface : IPowerToysModule + internal sealed class PowerAccentModuleInterface : IPowerToysModule { public string Name => "PowerAccent"; @@ -28,8 +33,6 @@ namespace PowerAccent.Core public void Enable() { - Disable(); - Process.Start("PowerToys.PowerAccent.exe", Environment.ProcessId.ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/RunnerV2/RunnerV2/NativeMethods.cs b/src/RunnerV2/RunnerV2/NativeMethods.cs index 71326724dc..0c412b1a94 100644 --- a/src/RunnerV2/RunnerV2/NativeMethods.cs +++ b/src/RunnerV2/RunnerV2/NativeMethods.cs @@ -116,6 +116,13 @@ namespace RunnerV2 [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool DispatchMessageW(ref MSG lpMsg); + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool PostMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + [LibraryImport("kernel32.dll")] + internal static partial ushort AddAtomW([MarshalAs(UnmanagedType.LPWStr)] string lpString); + internal struct MSG { public IntPtr HWnd; @@ -133,6 +140,7 @@ namespace RunnerV2 ICON_NOTIFY = 0x0800, WINDOWPOSCHANGING = 0x0046, DESTROY = 0x0002, + REFRESH_SETTINGS = 0x0400 + 2, } [DllImport("user32.dll")] diff --git a/src/RunnerV2/RunnerV2/Runner.cs b/src/RunnerV2/RunnerV2/Runner.cs index 0d4deefe48..d6a4adff66 100644 --- a/src/RunnerV2/RunnerV2/Runner.cs +++ b/src/RunnerV2/RunnerV2/Runner.cs @@ -31,43 +31,20 @@ namespace RunnerV2 InitializeTrayWindow(); } - private static List _successfullyAddedModules = []; + public static List LoadedModules { get; } = []; - public static List LoadedModules => _successfullyAddedModules; + public static FrozenSet ModulesToLoad { get; } = + [ + new ModuleInterfaces.AlwaysOnTopModuleInterface(), + new ModuleInterfaces.HostsModuleInterface(), + new ModuleInterfaces.PowerAccentModuleInterface(), + ]; internal static bool Run(Action afterInitializationAction) { TrayIconManager.StartTrayIcon(); - FrozenSet modulesToLoad = - [ - "PowerToys.AlwaysOnTopModuleInterface.dll", - "WinUI3Apps\\PowerToys.Hosts.dll", - "PowerAccent.Core.dll", - ]; - List failedModuleLoads = []; - - foreach (string module in modulesToLoad) - { - try - { - Assembly moduleAssembly = Assembly.LoadFrom(Path.GetFullPath(module)); - Type moduleInterfaceType = moduleAssembly.GetTypes().First(t => t.GetInterfaces().Any(i => i.Name.StartsWith(typeof(IPowerToysModule).Name, StringComparison.InvariantCulture))); - _successfullyAddedModules.Add((IPowerToysModule)Activator.CreateInstance(moduleInterfaceType)!); - } - catch (Exception e) - { - failedModuleLoads.Add(module); - Console.WriteLine($"Failed to load module {module}: {e.Message}"); - } - } - - if (failedModuleLoads.Count > 0) - { - MessageBox.Show("The following modules failed to load: \n- " + string.Join("\n- ", failedModuleLoads), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - - foreach (IPowerToysModule module in _successfullyAddedModules) + foreach (IPowerToysModule module in ModulesToLoad) { ToggleModuleStateBasedOnEnabledProperty(module); } @@ -81,6 +58,7 @@ namespace RunnerV2 private static readonly uint _taskbarCreatedMessage = RegisterWindowMessageW("TaskbarCreated"); + [STAThread] private static void MessageLoop() { while (GetMessageW(out MSG msg, IntPtr.Zero, 0, 0) != 0) @@ -88,6 +66,12 @@ namespace RunnerV2 TranslateMessage(ref msg); DispatchMessageW(ref msg); + // Supress duplicate handling of HOTKEY messages + if (msg.Message == (uint)WindowMessages.HOTKEY) + { + continue; + } + HandleMessage(msg.HWnd, msg.Message, (nint)msg.WParam, (nint)msg.LParam); } @@ -101,14 +85,14 @@ namespace RunnerV2 SettingsHelper.CloseSettingsWindow(); ElevationHelper.RestartIfScheudled(); - foreach (IPowerToysModule module in _successfullyAddedModules) + foreach (IPowerToysModule module in LoadedModules) { try { module.Disable(); - if (module.HotkeyEx is not null) + foreach (var hotkey in module.Hotkeys) { - HotkeyManager.DisableHotkey(module.HotkeyEx); + HotkeyManager.DisableHotkey(hotkey.Key); } } catch (Exception e) @@ -130,9 +114,14 @@ namespace RunnerV2 /* Todo: conflict manager */ - if (module.HotkeyEx is not null) + foreach (var hotkey in module.Hotkeys) { - HotkeyManager.EnableHotkey(module.HotkeyEx, module.OnHotkey); + HotkeyManager.EnableHotkey(hotkey.Key, hotkey.Value); + } + + if (!LoadedModules.Contains(module)) + { + LoadedModules.Add(module); } } catch (Exception e) @@ -147,10 +136,12 @@ namespace RunnerV2 { module.Disable(); - if (module.HotkeyEx is not null) + foreach (var hotkey in module.Hotkeys) { - HotkeyManager.DisableHotkey(module.HotkeyEx); + HotkeyManager.DisableHotkey(hotkey.Key); } + + LoadedModules.Remove(module); } catch (Exception e) { @@ -204,21 +195,12 @@ namespace RunnerV2 } } - private static bool _handledShortcut; - private static IntPtr HandleMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { switch (msg) { case (uint)WindowMessages.HOTKEY: - if (_handledShortcut) - { - _handledShortcut = false; - break; - } - HotkeyManager.ProcessHotkey((nuint)wParam); - _handledShortcut = true; break; case (uint)WindowMessages.ICON_NOTIFY: TrayIconManager.ProcessTrayIconMessage(lParam); @@ -231,6 +213,13 @@ namespace RunnerV2 break; case (uint)WindowMessages.DESTROY: Close(); + break; + case (uint)WindowMessages.REFRESH_SETTINGS: + foreach (IPowerToysModule module in ModulesToLoad) + { + ToggleModuleStateBasedOnEnabledProperty(module); + } + break; default: if (msg == _taskbarCreatedMessage) diff --git a/src/common/ManagedCommon/HotkeyEx.cs b/src/common/ManagedCommon/HotkeyEx.cs index 10ae8c5c55..eb8448af64 100644 --- a/src/common/ManagedCommon/HotkeyEx.cs +++ b/src/common/ManagedCommon/HotkeyEx.cs @@ -4,5 +4,5 @@ namespace ManagedCommon { - public record HotkeyEx(ushort ModifiersMask, ushort VkCode); + public record HotkeyEx(ushort ModifiersMask, ushort VkCode, int Identifier); } diff --git a/src/common/ManagedCommon/IPowerToysModule.cs b/src/common/ManagedCommon/IPowerToysModule.cs index d502079938..6b550df469 100644 --- a/src/common/ManagedCommon/IPowerToysModule.cs +++ b/src/common/ManagedCommon/IPowerToysModule.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text.Json; using PowerToys.GPOWrapper; @@ -20,10 +21,12 @@ namespace ManagedCommon public GpoRuleConfigured GpoRuleConfigured { get; } - public virtual HotkeyEx? HotkeyEx => null; + public Dictionary Hotkeys { get => []; } - public virtual Action OnHotkey => () => { }; + public Dictionary CustomActions { get => []; } - public virtual Action OnSettingsChanged(string settingsKind, JsonElement jsonProperties) => () => { }; + public void OnSettingsChanged(string settingsKind, JsonElement jsonProperties) + { + } } } diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.csproj b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.csproj deleted file mode 100644 index bab66629ab..0000000000 --- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - - PowerToys Alway on Top module interface - PowerToys.AlwaysOnTopModuleInterface - ..\..\..\..\$(Platform)\$(Configuration) - false - false - enable - - - - - - - - diff --git a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs index 29e0b98b4f..4ac309ed8e 100644 --- a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs +++ b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs @@ -340,7 +340,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library modifiers += 0x0008; } - return new HotkeyEx(modifiers, (ushort)settings.Code); + return new HotkeyEx(modifiers, (ushort)settings.Code, 0); } } }