diff --git a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj
index 86039b95e0..794810cd71 100644
--- a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj
+++ b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj
@@ -52,15 +52,15 @@
Create
-
-
- {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
-
-
- {6955446d-23f7-4023-9bb3-8657f904af99}
-
-
- {8affa899-0b73-49ec-8c50-0fadda57b2fc}
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+ {8affa899-0b73-49ec-8c50-0fadda57b2fc}
@@ -85,4 +85,4 @@
-
\ No newline at end of file
+
diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp
index 11fe776281..499b4a9693 100644
--- a/src/modules/keyboardmanager/dll/dllmain.cpp
+++ b/src/modules/keyboardmanager/dll/dllmain.cpp
@@ -28,12 +28,24 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp
return TRUE;
}
+namespace
+{
+ const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
+ const wchar_t JSON_KEY_WIN[] = L"win";
+ const wchar_t JSON_KEY_ALT[] = L"alt";
+ const wchar_t JSON_KEY_CTRL[] = L"ctrl";
+ const wchar_t JSON_KEY_SHIFT[] = L"shift";
+ const wchar_t JSON_KEY_CODE[] = L"code";
+ const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut";
+}
+
// Implement the PowerToy Module Interface and all the required methods.
class KeyboardManager : public PowertoyModuleIface
{
private:
// The PowerToy state.
bool m_enabled = false;
+ bool m_active = false;
// The PowerToy name that will be shown in the settings.
const std::wstring app_name = GET_RESOURCE_STRING(IDS_KEYBOARDMANAGER);
@@ -41,10 +53,146 @@ private:
//contains the non localized key of the powertoy
std::wstring app_key = KeyboardManagerConstants::ModuleName;
+ // Hotkey for toggling the module
+ Hotkey m_hotkey = { .key = 0 };
+
+ ULONGLONG m_lastHotkeyToggleTime = 0;
+
HANDLE m_hProcess = nullptr;
HANDLE m_hTerminateEngineEvent = nullptr;
+ void refresh_process_state()
+ {
+ if (m_hProcess && WaitForSingleObject(m_hProcess, 0) != WAIT_TIMEOUT)
+ {
+ CloseHandle(m_hProcess);
+ m_hProcess = nullptr;
+ m_active = false;
+ }
+ }
+
+ bool start_engine()
+ {
+ refresh_process_state();
+ if (m_hProcess)
+ {
+ m_active = true;
+ return true;
+ }
+
+ if (!m_hTerminateEngineEvent)
+ {
+ Logger::error(L"Cannot start keyboard manager engine because terminate event is not available");
+ m_active = false;
+ return false;
+ }
+
+ unsigned long powertoys_pid = GetCurrentProcessId();
+ std::wstring executable_args = std::to_wstring(powertoys_pid);
+
+ SHELLEXECUTEINFOW sei{ sizeof(sei) };
+ sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
+ sei.lpFile = L"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe";
+ sei.nShow = SW_SHOWNORMAL;
+ sei.lpParameters = executable_args.data();
+ if (ShellExecuteExW(&sei) == false)
+ {
+ Logger::error(L"Failed to start keyboard manager engine");
+ auto message = get_last_error_message(GetLastError());
+ if (message.has_value())
+ {
+ Logger::error(message.value());
+ }
+
+ m_active = false;
+ return false;
+ }
+
+ m_hProcess = sei.hProcess;
+ if (m_hProcess)
+ {
+ SetPriorityClass(m_hProcess, REALTIME_PRIORITY_CLASS);
+ m_active = true;
+ return true;
+ }
+
+ m_active = false;
+ return false;
+ }
+
+ void stop_engine()
+ {
+ refresh_process_state();
+ if (!m_hProcess)
+ {
+ m_active = false;
+ return;
+ }
+
+ SetEvent(m_hTerminateEngineEvent);
+ auto waitResult = WaitForSingleObject(m_hProcess, 1500);
+ if (waitResult == WAIT_TIMEOUT)
+ {
+ TerminateProcess(m_hProcess, 0);
+ WaitForSingleObject(m_hProcess, 500);
+ }
+
+ CloseHandle(m_hProcess);
+ m_hProcess = nullptr;
+ ResetEvent(m_hTerminateEngineEvent);
+ m_active = false;
+ }
+
+ void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
+ {
+ auto settingsObject = settings.get_raw_json();
+ if (settingsObject.GetView().Size())
+ {
+ try
+ {
+ auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES)
+ .GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
+ m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
+ m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
+ m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
+ m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
+ m_hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
+ }
+ catch (...)
+ {
+ Logger::error("Failed to initialize Keyboard Manager toggle shortcut");
+ }
+ }
+
+ if (!m_hotkey.key)
+ {
+ // Set default: Win+Shift+K
+ m_hotkey.win = true;
+ m_hotkey.shift = true;
+ m_hotkey.ctrl = false;
+ m_hotkey.alt = false;
+ m_hotkey.key = 'K';
+ }
+ }
+
+ // Load the settings file.
+ void init_settings()
+ {
+ try
+ {
+ // Load and parse the settings file for this PowerToy.
+ PowerToysSettings::PowerToyValues settings =
+ PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
+ parse_hotkey(settings);
+ }
+ catch (std::exception&)
+ {
+ Logger::warn(L"An exception occurred while loading the settings file");
+ // Error while loading from the settings file. Let default values stay as they are.
+ }
+ }
+
public:
// Constructor
KeyboardManager()
@@ -65,8 +213,20 @@ public:
Logger::error(message.value());
}
}
+
+ init_settings();
};
+ ~KeyboardManager()
+ {
+ stop_engine();
+ if (m_hTerminateEngineEvent)
+ {
+ CloseHandle(m_hTerminateEngineEvent);
+ m_hTerminateEngineEvent = nullptr;
+ }
+ }
+
// Destroy the powertoy and free memory
virtual void destroy() override
{
@@ -117,6 +277,7 @@ public:
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
+ parse_hotkey(values);
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
@@ -134,33 +295,7 @@ public:
m_enabled = true;
// Log telemetry
Trace::EnableKeyboardManager(true);
-
- unsigned long powertoys_pid = GetCurrentProcessId();
- std::wstring executable_args = L"";
- executable_args.append(std::to_wstring(powertoys_pid));
-
- SHELLEXECUTEINFOW sei{ sizeof(sei) };
- sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
- sei.lpFile = L"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe";
- sei.nShow = SW_SHOWNORMAL;
- sei.lpParameters = executable_args.data();
- if (ShellExecuteExW(&sei) == false)
- {
- Logger::error(L"Failed to start keyboard manager engine");
- auto message = get_last_error_message(GetLastError());
- if (message.has_value())
- {
- Logger::error(message.value());
- }
- }
- else
- {
- m_hProcess = sei.hProcess;
- if (m_hProcess)
- {
- SetPriorityClass(m_hProcess, REALTIME_PRIORITY_CLASS);
- }
- }
+ start_engine();
}
// Disable the powertoy
@@ -169,15 +304,7 @@ public:
m_enabled = false;
// Log telemetry
Trace::EnableKeyboardManager(false);
-
- if (m_hProcess)
- {
- SetEvent(m_hTerminateEngineEvent);
- WaitForSingleObject(m_hProcess, 1500);
-
- TerminateProcess(m_hProcess, 0);
- m_hProcess = nullptr;
- }
+ stop_engine();
}
// Returns if the powertoys is enabled
@@ -192,9 +319,54 @@ public:
return false;
}
+ // Return the invocation hotkey for toggling
+ virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
+ {
+ if (m_hotkey.key)
+ {
+ if (hotkeys && buffer_size >= 1)
+ {
+ hotkeys[0] = m_hotkey;
+ }
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ // Process the hotkey event
+ virtual bool on_hotkey(size_t /*hotkeyId*/) override
+ {
+ if (!m_enabled)
+ {
+ return false;
+ }
+
+ constexpr ULONGLONG hotkeyToggleDebounceMs = 500;
+ const auto now = GetTickCount64();
+ if (now - m_lastHotkeyToggleTime < hotkeyToggleDebounceMs)
+ {
+ return true;
+ }
+ m_lastHotkeyToggleTime = now;
+
+ refresh_process_state();
+ if (m_active)
+ {
+ stop_engine();
+ }
+ else
+ {
+ start_engine();
+ }
+
+ return true;
+ }
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new KeyboardManager();
-}
\ No newline at end of file
+}
diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs
index 25a84dbb2c..1da3bfd72b 100644
--- a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs
@@ -21,12 +21,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnoreAttribute]
public GenericProperty> KeyboardConfigurations { get; set; }
+ public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B);
+
public KeyboardManagerProperties()
{
+ ToggleShortcut = DefaultToggleShortcut;
KeyboardConfigurations = new GenericProperty>(new List { "default", });
ActiveConfiguration = new GenericProperty("default");
}
+ public HotkeySettings ToggleShortcut { get; set; }
+
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs
index 5682d6d865..84ba811b90 100644
--- a/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs
@@ -2,8 +2,9 @@
// 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.Collections.Generic;
using System.Text.Json.Serialization;
-
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
@@ -32,5 +33,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
return false;
}
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ToggleShortcut,
+ value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut,
+ "Toggle_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
}
}
diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
index 36fd08ecd2..9ad0af9d68 100644
--- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
+++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
@@ -10,7 +10,6 @@ using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
-using SettingsUILibrary = Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
@@ -24,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(IList))]
+[JsonSerializable(typeof(KeyboardManagerSettings))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
index bfa2b2495c..b2fef9d908 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
@@ -70,6 +70,13 @@
+
+
+
+
Customize the shortcut to activate this module
+
+ Toggle shortcut
+
+
+ Use a shortcut to toggle this module on or off (note that the Settings UI will not update)
+
Paste as plain text directly
diff --git a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs
index d7bf9862bc..0c142cf6c0 100644
--- a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs
@@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
@@ -18,18 +19,20 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
+using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.Utilities;
using Microsoft.Win32;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
- public partial class KeyboardManagerViewModel : Observable
+ public partial class KeyboardManagerViewModel : PageViewModelBase
{
private GeneralSettings GeneralSettingsConfig { get; set; }
private readonly SettingsUtils _settingsUtils;
- private const string PowerToyName = KeyboardManagerSettings.ModuleName;
+ protected override string ModuleName => KeyboardManagerSettings.ModuleName;
+
private const string JsonFileType = ".json";
// Default editor path. Can be removed once the new WinUI3 editor is released.
@@ -74,15 +77,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
- if (_settingsUtils.SettingsExists(PowerToyName))
+ if (_settingsUtils.SettingsExists(ModuleName))
{
try
{
- Settings = _settingsUtils.GetSettingsOrDefault(PowerToyName);
+ Settings = _settingsUtils.GetSettingsOrDefault(ModuleName);
}
catch (Exception e)
{
- Logger.LogError($"Exception encountered while reading {PowerToyName} settings.", e);
+ Logger.LogError($"Exception encountered while reading {ModuleName} settings.", e);
#if DEBUG
if (e is ArgumentException || e is ArgumentNullException || e is PathTooLongException)
{
@@ -100,7 +103,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
else
{
Settings = new KeyboardManagerSettings();
- _settingsUtils.SaveSettings(Settings.ToJsonString(), PowerToyName);
+ _settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
}
}
@@ -174,6 +177,41 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public override Dictionary GetAllHotkeySettings()
+ {
+ var hotkeysDict = new Dictionary
+ {
+ [ModuleName] = [ToggleShortcut],
+ };
+
+ return hotkeysDict;
+ }
+
+ public HotkeySettings ToggleShortcut
+ {
+ get => Settings.Properties.ToggleShortcut;
+ set
+ {
+ if (Settings.Properties.ToggleShortcut != value)
+ {
+ Settings.Properties.ToggleShortcut = value ?? Settings.Properties.DefaultToggleShortcut;
+ OnPropertyChanged(nameof(ToggleShortcut));
+ NotifySettingsChanged();
+ }
+ }
+ }
+
+ private void NotifySettingsChanged()
+ {
+ // Using InvariantCulture as this is an IPC message
+ SendConfigMSG(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
+ ModuleName,
+ JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.KeyboardManagerSettings)));
+ }
+
public static List CombineShortcutLists(List globalShortcutList, List appSpecificShortcutList)
{
string allAppsDescription = "All Apps";
@@ -256,13 +294,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (editor != null && editor.HasExited)
{
- Logger.LogInfo($"Previous instance of {PowerToyName} editor exited");
+ Logger.LogInfo($"Previous instance of {ModuleName} editor exited");
editor = null;
}
if (editor != null)
{
- Logger.LogInfo($"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front");
+ Logger.LogInfo($"The {ModuleName} editor instance {editor.Id} exists. Bringing the process to the front");
BringProcessToFront(editor);
return;
}
@@ -298,14 +336,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
string path = Path.Combine(Environment.CurrentDirectory, editorPath);
- Logger.LogInfo($"Starting {PowerToyName} editor from {path}");
+ Logger.LogInfo($"Starting {ModuleName} editor from {path}");
// InvariantCulture: type represents the KeyboardManagerEditorType enum value
editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}");
}
catch (Exception e)
{
- Logger.LogError($"Exception encountered when opening an {PowerToyName} editor", e);
+ Logger.LogError($"Exception encountered when opening an {ModuleName} editor", e);
}
}
@@ -338,7 +376,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
while (!readSuccessfully && !ts.IsCancellationRequested)
{
- profileExists = _settingsUtils.SettingsExists(PowerToyName, fileName);
+ profileExists = _settingsUtils.SettingsExists(ModuleName, fileName);
if (!profileExists)
{
break;
@@ -347,12 +385,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
try
{
- _profile = _settingsUtils.GetSettingsOrDefault(PowerToyName, fileName);
+ _profile = _settingsUtils.GetSettingsOrDefault(ModuleName, fileName);
readSuccessfully = true;
}
catch (Exception e)
{
- Logger.LogError($"Exception encountered when reading {PowerToyName} settings", e);
+ Logger.LogError($"Exception encountered when reading {ModuleName} settings", e);
}
}
@@ -379,23 +417,23 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (!completedInTime)
{
- Logger.LogError($"Timeout encountered when loading {PowerToyName} profile");
+ Logger.LogError($"Timeout encountered when loading {ModuleName} profile");
}
}
catch (Exception e)
{
// Failed to load the configuration.
- Logger.LogError($"Exception encountered when loading {PowerToyName} profile", e);
+ Logger.LogError($"Exception encountered when loading {ModuleName} profile", e);
success = false;
}
if (!profileExists)
{
- Logger.LogInfo($"Couldn't load {PowerToyName} profile because it doesn't exist");
+ Logger.LogInfo($"Couldn't load {ModuleName} profile because it doesn't exist");
}
else if (!success)
{
- Logger.LogError($"Couldn't load {PowerToyName} profile");
+ Logger.LogError($"Couldn't load {ModuleName} profile");
}
return success;