[Keyboard Manager] Toggle module hotkey/shortcut (#42472)

## Summary of the Pull Request

Adds a keyboard shortcut to be able to toggle the Keyboard Manager
module on and off.

## PR Checklist

- [x] Closes: #4879 
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments

Modeled the changes and addition of a global shortcut using the Color
Picker module.

Notes:
- This uses `KeyboardManagerSettings` and the associated .json settings
file. I don't think there's anything else in this module that uses this.
- The default key binding for this is `winkey + shift + k`
- I've had to update the `KeyboardManagerViewModel` to extend
`PageViewModelBase` as opposed to `Observable` to get it to work. But I
will say that there were some things in here that I didn't fully dig
into, so let me know if there's any potential things I'm missing.
- I'm not too sure how to update the Settings UI after a hotkey is
pressed (pressing the hotkey currently will not show the module being
toggled off) - can't find a good way to refresh the settings ui after
enabling/disabling the module. Any pointers here would be appreciated!



## Validation Steps Performed
- Manually validated the following items:
  - Using the default shortcut (`winkey + shift + k`)
  - Changing the shortcut
  - Ensuring the change is persistent

## Media


https://github.com/user-attachments/assets/e471b8df-787a-441e-b9e0-330361865c76

---------

Co-authored-by: Weike Qu <weikequ@get-stride.com>
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
Co-authored-by: vanzue <vanzue@outlook.com>
Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Weike Qu
2026-02-12 04:01:40 -08:00
committed by GitHub
parent 587385d879
commit f88a4908ac
8 changed files with 308 additions and 66 deletions

View File

@@ -21,12 +21,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnoreAttribute]
public GenericProperty<List<string>> KeyboardConfigurations { get; set; }
public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B);
public KeyboardManagerProperties()
{
ToggleShortcut = DefaultToggleShortcut;
KeyboardConfigurations = new GenericProperty<List<string>>(new List<string> { "default", });
ActiveConfiguration = new GenericProperty<string>("default");
}
public HotkeySettings ToggleShortcut { get; set; }
public string ToJsonString()
{
return JsonSerializer.Serialize(this);

View File

@@ -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<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ToggleShortcut,
value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut,
"Toggle_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
}
}

View File

@@ -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<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(KeyboardManagerSettings))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]

View File

@@ -70,6 +70,13 @@
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<tkcontrols:SettingsCard
Name="ToggleShortcut"
x:Uid="KeyboardManager_Toggle_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.ToggleShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<controls:SettingsGroup x:Uid="KeyboardManager_Keys" IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="KeyboardManagerRemapKeyboardButton"

View File

@@ -1876,6 +1876,12 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
<data name="Activation_Shortcut.Description" xml:space="preserve">
<value>Customize the shortcut to activate this module</value>
</data>
<data name="KeyboardManager_Toggle_Shortcut.Header" xml:space="preserve">
<value>Toggle shortcut</value>
</data>
<data name="KeyboardManager_Toggle_Shortcut.Description" xml:space="preserve">
<value>Use a shortcut to toggle this module on or off (note that the Settings UI will not update)</value>
</data>
<data name="PasteAsPlainText_Shortcut.Header" xml:space="preserve">
<value>Paste as plain text directly</value>
</data>

View File

@@ -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<KeyboardManagerSettings>(PowerToyName);
Settings = _settingsUtils.GetSettingsOrDefault<KeyboardManagerSettings>(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<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[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<AppSpecificKeysDataModel> CombineShortcutLists(List<KeysDataModel> globalShortcutList, List<AppSpecificKeysDataModel> 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<KeyboardManagerProfile>(PowerToyName, fileName);
_profile = _settingsUtils.GetSettingsOrDefault<KeyboardManagerProfile>(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;