mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-02 08:28:55 +02:00
Compare commits
12 Commits
powerscrip
...
yuleng/kbm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71c1b5311c | ||
|
|
5619bc95c0 | ||
|
|
2b4fb9e730 | ||
|
|
d1091b3a6a | ||
|
|
0e137a3213 | ||
|
|
554c207776 | ||
|
|
7fe58e8cdb | ||
|
|
7c7f8fb43a | ||
|
|
7bbead48a4 | ||
|
|
48e56690f7 | ||
|
|
a67cfd7f39 | ||
|
|
679eb22532 |
8
.github/actions/spell-check/expect.txt
vendored
8
.github/actions/spell-check/expect.txt
vendored
@@ -126,6 +126,7 @@ boxmodel
|
||||
BPBF
|
||||
bpmf
|
||||
bpp
|
||||
brb
|
||||
Browsable
|
||||
BROWSEINFO
|
||||
bsd
|
||||
@@ -750,6 +751,7 @@ KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
Kybd
|
||||
Laf
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
@@ -941,6 +943,7 @@ mstsc
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
Mtnhu
|
||||
multimonitor
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
@@ -1470,6 +1473,7 @@ SIZENWSE
|
||||
SIZEWE
|
||||
SKEXP
|
||||
SKIPOWNPROCESS
|
||||
skm
|
||||
sku
|
||||
SLGP
|
||||
sln
|
||||
@@ -1608,6 +1612,7 @@ templatenamespace
|
||||
testprocess
|
||||
TEXCOORD
|
||||
TEXTINCLUDE
|
||||
textinputmethod
|
||||
tfopen
|
||||
tgz
|
||||
themeresources
|
||||
@@ -1716,6 +1721,7 @@ vcruntime
|
||||
vcvars
|
||||
VDesktop
|
||||
vdupq
|
||||
VEb
|
||||
VERBSONLY
|
||||
VERBW
|
||||
VERIFYCONTEXT
|
||||
@@ -1792,6 +1798,7 @@ windowssearch
|
||||
windowssettings
|
||||
WINDOWSTYLES
|
||||
WINDOWSTYLESICON
|
||||
windowsupdate
|
||||
winerror
|
||||
WINEVENT
|
||||
winget
|
||||
@@ -2056,6 +2063,7 @@ framechanged
|
||||
FRestore
|
||||
fsanitize
|
||||
ftps
|
||||
FTt
|
||||
fuzzingtesting
|
||||
fxf
|
||||
gameid
|
||||
|
||||
71
CLAUDE.md
Normal file
71
CLAUDE.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Communication
|
||||
|
||||
Always respond in Chinese (中文).
|
||||
|
||||
## Project Overview
|
||||
|
||||
PowerToys is a collection of Windows productivity utilities written in C++ and C#. The main solution is `PowerToys.slnx`. Each utility is a "module" loaded by the Runner via a standardized DLL interface.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Runner** (`src/runner/`): Main executable (PowerToys.exe). Loads module DLLs, manages hotkeys, shows tray icon, bridges modules to Settings UI via named pipes (JSON IPC).
|
||||
- **Settings UI** (`src/settings-ui/`): WinUI/WPF configuration app. Communicates with Runner over named pipes. Changes to IPC contracts must update both sides in the same PR.
|
||||
- **Modules** (`src/modules/`): ~30 individual utilities, each implementing the module interface (`src/modules/interface/`). Four module types: simple (self-contained DLL), external app launcher (separate process + IPC), context handler (shell extension), registry-based (preview handlers).
|
||||
- **Common** (`src/common/`): Shared libraries — logging, IPC, settings serialization, DPI utilities, telemetry, JSON/string helpers. Changes here affect the entire codebase.
|
||||
- **Installer** (`installer/`): WiX-based installer projects. Separate solution at `installer/PowerToysSetup.slnx`.
|
||||
|
||||
## Build Commands
|
||||
|
||||
Prerequisites: Visual Studio 2022 17.4+ or VS 2026, Windows 10 1803+. Initialize submodules once: `git submodule update --init --recursive`.
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| First build / NuGet restore | `tools\build\build-essentials.cmd` |
|
||||
| Build current folder's project | `cd` to the `.csproj`/`.vcxproj` folder, then `tools\build\build.cmd` |
|
||||
| Build with options | `tools\build\build.ps1 -Platform x64 -Configuration Release` |
|
||||
| Full installer build | `tools\build\build-installer.ps1 -Platform x64 -Configuration Release -PerUser true -InstallerSuffix wix5` |
|
||||
| Format XAML | `.\.pipelines\applyXamlStyling.ps1 -Main` |
|
||||
| Format C++ | `src\codeAnalysis\format_sources.ps1` (formats git-modified files) |
|
||||
|
||||
Build logs appear next to the solution/project: `build.<config>.<platform>.errors.log` (check first), `.all.log`, `.trace.binlog`.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Do NOT use `dotnet test`** — use VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters.
|
||||
- Build the test project first (exit code 0) before running tests.
|
||||
- Test projects are named `<Product>*UnitTests` or `<Product>*UITests`, typically sibling folders or 1-2 levels up from the product code.
|
||||
- UI Tests require WinAppDriver v1.2.1 and Developer Mode enabled.
|
||||
|
||||
## Style and Formatting
|
||||
|
||||
- **C++**: `src/.clang-format` — auto-format with `Ctrl+K Ctrl+D` in VS or run `format_sources.ps1`.
|
||||
- **C#**: `src/.editorconfig` + StyleCop.Analyzers.
|
||||
- **XAML**: XamlStyler — VS extension or `applyXamlStyling.ps1`.
|
||||
- Follow existing style in modified files. New code should follow Modern C++ / C++ Core Guidelines.
|
||||
|
||||
## Logging
|
||||
|
||||
- **C++**: spdlog via `init_logger()`. Include `spdlog.props` in `.vcxproj`. Use `Logger::info/warn/error/debug`.
|
||||
- **C#**: `ManagedCommon.Logger`. Call `Logger.InitializeLogger("\\Module\\Logs")` at startup. Use `Logger.LogInfo/LogWarning/LogError/LogDebug`.
|
||||
- Log files: `%LOCALAPPDATA%\Microsoft\PowerToys\Logs`. Low-privilege processes use `%USERPROFILE%\AppData\LocalLow\Microsoft\PowerToys`.
|
||||
- No logging in hot paths (hooks, tight loops, timers).
|
||||
|
||||
## Key Constraints
|
||||
|
||||
- Atomic PRs: one logical change per PR, no drive-by refactors.
|
||||
- IPC/JSON contract changes must update both `src/runner/` and `src/settings-ui/` together.
|
||||
- Changes to `src/common/` public APIs: grep the entire codebase for usages and update all callers.
|
||||
- New third-party dependencies must be MIT-licensed (or PM-approved) and added to `NOTICE.md`.
|
||||
- Settings schema changes require migration logic and serialization tests.
|
||||
- New modules with file I/O or user input must include fuzzing tests.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Architecture](doc/devdocs/core/architecture.md), [Runner](doc/devdocs/core/runner.md), [Settings](doc/devdocs/core/settings/readme.md)
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md), [Style](doc/devdocs/development/style.md), [Logging](doc/devdocs/development/logging.md)
|
||||
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md), [Module Interface](doc/devdocs/modules/interface.md)
|
||||
- [AGENTS.md](AGENTS.md) — full AI contributor guide with detailed conventions
|
||||
@@ -68,6 +68,16 @@
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.KeyboardManagerEngine" Executable="KeyboardManagerEngine\PowerToys.KeyboardManagerEngine.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.KeyboardManagerEngine"
|
||||
Description="PowerToys Keyboard Manager Engine"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.CmdPalExtension" Executable="Microsoft.CmdPal.Ext.PowerToys.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.CommandPaletteExtension"
|
||||
|
||||
@@ -22,5 +22,14 @@ namespace ManagedCommon
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build > 22000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TSF4 (Text Services Framework 4) APIs require Windows 11 build 20000+.
|
||||
/// TODO: Confirm the exact minimum build number once finalized.
|
||||
/// </summary>
|
||||
public static bool IsTsf4Supported()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 10000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,6 +678,50 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AddExpandMapping(void* config, const wchar_t* abbreviation, int triggerKey, const wchar_t* expandedText, const wchar_t* targetApp)
|
||||
{
|
||||
auto mappingConfig = static_cast<MappingConfiguration*>(config);
|
||||
if (!abbreviation || !expandedText)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ExpandMapping mapping;
|
||||
mapping.abbreviation = abbreviation;
|
||||
mapping.triggerKey = static_cast<DWORD>(triggerKey);
|
||||
mapping.expandedText = expandedText;
|
||||
mapping.appName = targetApp ? targetApp : L"";
|
||||
|
||||
mappingConfig->expandMappings.push_back(std::move(mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeleteExpandMapping(void* config, const wchar_t* abbreviation, const wchar_t* targetApp)
|
||||
{
|
||||
auto mappingConfig = static_cast<MappingConfiguration*>(config);
|
||||
if (!abbreviation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring abbrev = abbreviation;
|
||||
std::wstring app = targetApp ? targetApp : L"";
|
||||
|
||||
auto& mappings = mappingConfig->expandMappings;
|
||||
auto it = std::remove_if(mappings.begin(), mappings.end(), [&](const ExpandMapping& m) {
|
||||
return _wcsicmp(m.abbreviation.c_str(), abbrev.c_str()) == 0 &&
|
||||
_wcsicmp(m.appName.c_str(), app.c_str()) == 0;
|
||||
});
|
||||
|
||||
if (it != mappings.end())
|
||||
{
|
||||
mappings.erase(it, mappings.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to delete a shortcut remapping
|
||||
bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp)
|
||||
{
|
||||
|
||||
@@ -79,6 +79,9 @@ extern "C"
|
||||
__declspec(dllexport) bool IsShortcutIllegal(const wchar_t* shortcutKeys);
|
||||
__declspec(dllexport) bool AreShortcutsEqual(const wchar_t* lShort, const wchar_t* rShort);
|
||||
|
||||
__declspec(dllexport) bool AddExpandMapping(void* config, const wchar_t* abbreviation, int triggerKey, const wchar_t* expandedText, const wchar_t* targetApp);
|
||||
__declspec(dllexport) bool DeleteExpandMapping(void* config, const wchar_t* abbreviation, const wchar_t* targetApp);
|
||||
|
||||
__declspec(dllexport) bool DeleteSingleKeyRemap(void* config, int originalKey);
|
||||
__declspec(dllexport) bool DeleteSingleKeyToTextRemap(void* config, int originalKey);
|
||||
__declspec(dllexport) bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp);
|
||||
|
||||
@@ -81,6 +81,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
ActionType.Shortcut => "\uEDA7",
|
||||
ActionType.MouseClick => "\uE962",
|
||||
ActionType.Url => "\uE774",
|
||||
ActionType.Expand => "\uE8C8",
|
||||
_ => "\uE8A5",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
<TextBlock x:Uid="TriggerType_KeyOrShortcut_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Name="TriggerType_ExpandItem" x:Uid="TriggerType_Expand" Tag="Expand">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="TriggerType_Expand_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<!--
|
||||
<ComboBoxItem x:Uid="TriggerType_Mouse" Tag="Mouse">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -113,6 +119,44 @@
|
||||
IsChecked="True" />
|
||||
</StackPanel>
|
||||
</tkcontrols:Case>
|
||||
<!-- Expand Trigger -->
|
||||
<tkcontrols:Case Value="Expand">
|
||||
<StackPanel Orientation="Vertical" Spacing="8">
|
||||
<TextBox
|
||||
x:Name="ExpandAbbreviationBox"
|
||||
x:Uid="ExpandAbbreviationBox"
|
||||
Background="{ThemeResource TextControlBackgroundFocused}"
|
||||
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
|
||||
GotFocus="ExpandAbbreviationBox_GotFocus"
|
||||
TextChanged="ExpandAbbreviationBox_TextChanged" />
|
||||
<TextBlock
|
||||
x:Uid="ExpandTriggerKeyLabel"
|
||||
Margin="0,8,0,0"
|
||||
FontWeight="SemiBold" />
|
||||
<ToggleButton
|
||||
x:Name="ExpandTriggerKeyToggleBtn"
|
||||
MinHeight="48"
|
||||
Padding="8,12,8,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Checked="ExpandTriggerKeyToggleBtn_Checked"
|
||||
Style="{StaticResource CustomShortcutToggleButtonStyle}"
|
||||
Unchecked="ExpandTriggerKeyToggleBtn_Unchecked">
|
||||
<ToggleButton.Content>
|
||||
<commoncontrols:KeyVisual
|
||||
x:Name="ExpandTriggerKeyVisual"
|
||||
Padding="8"
|
||||
Background="{ThemeResource ControlFillColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Content="Space"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource DefaultKeyVisualStyle}" />
|
||||
</ToggleButton.Content>
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</tkcontrols:Case>
|
||||
<!-- Mouse Button Trigger -->
|
||||
<tkcontrols:Case Value="Mouse">
|
||||
<ComboBox
|
||||
@@ -188,10 +232,10 @@
|
||||
<TextBlock x:Uid="ActionType_KeyOrShortcut_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_Text" Tag="Text">
|
||||
<ComboBoxItem x:Name="ActionTypeTextItem" x:Uid="ActionType_Text" Tag="Text">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_Text_Text" />
|
||||
<FontIcon x:Name="ActionTypeTextIcon" FontSize="14" Glyph="" />
|
||||
<TextBlock x:Name="ActionTypeTextLabel" x:Uid="ActionType_Text_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_OpenUrl" Tag="OpenUrl">
|
||||
@@ -229,7 +273,7 @@
|
||||
<tkcontrols:SwitchPresenter
|
||||
x:Name="ActionSwitchPresenter"
|
||||
TargetType="x:String"
|
||||
Value="{Binding SelectedItem.Tag, ElementName=ActionTypeComboBox}">
|
||||
Value="KeyOrShortcut">
|
||||
<!-- Key or Shortcut Action -->
|
||||
<tkcontrols:Case Value="KeyOrShortcut">
|
||||
<ToggleButton
|
||||
@@ -384,6 +428,7 @@
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</tkcontrols:Case>
|
||||
<!-- Replace With uses the Text case above with label override -->
|
||||
<!-- Mouse Click Action (Placeholder) -->
|
||||
<tkcontrols:Case Value="MouseClick">
|
||||
<TextBlock
|
||||
|
||||
@@ -43,6 +43,15 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
private bool _textContentDirty;
|
||||
private bool _urlPathDirty;
|
||||
private bool _programPathDirty;
|
||||
private bool _expandAbbreviationDirty;
|
||||
|
||||
// Expand trigger key tracking
|
||||
private string _expandTriggerKey = "Space";
|
||||
private bool _isCapturingExpandTriggerKey;
|
||||
|
||||
// Saved label/icon for restoring "Text" action item after Expand mode
|
||||
private string? _savedTextLabel;
|
||||
private string? _savedTextIconGlyph;
|
||||
|
||||
public bool AllowChords { get; set; } = true;
|
||||
|
||||
@@ -66,6 +75,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
KeyOrShortcut,
|
||||
Mouse,
|
||||
Expand,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,6 +88,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
OpenUrl,
|
||||
OpenApp,
|
||||
MouseClick,
|
||||
ReplaceWith,
|
||||
Disable,
|
||||
}
|
||||
|
||||
@@ -108,6 +119,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
return item.Tag?.ToString() switch
|
||||
{
|
||||
"Mouse" => TriggerType.Mouse,
|
||||
"Expand" => TriggerType.Expand,
|
||||
_ => TriggerType.KeyOrShortcut,
|
||||
};
|
||||
}
|
||||
@@ -123,6 +135,12 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
// When in Expand trigger mode, the action is always ReplaceWith
|
||||
if (CurrentTriggerType == TriggerType.Expand)
|
||||
{
|
||||
return ActionType.ReplaceWith;
|
||||
}
|
||||
|
||||
if (ActionTypeComboBox?.SelectedItem is ComboBoxItem item)
|
||||
{
|
||||
return item.Tag?.ToString() switch
|
||||
@@ -163,6 +181,12 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
RaiseValidationStateChanged();
|
||||
};
|
||||
|
||||
// Disable Text Expand option when TSF4 API is not available on this OS
|
||||
if (!ManagedCommon.OSVersionHelper.IsTsf4Supported())
|
||||
{
|
||||
TriggerType_ExpandItem.IsEnabled = false;
|
||||
}
|
||||
|
||||
this.Unloaded += UnifiedMappingControl_Unloaded;
|
||||
}
|
||||
|
||||
@@ -200,13 +224,26 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
string? tag = item.Tag?.ToString();
|
||||
|
||||
// Cleanup keyboard hook when switching to mouse
|
||||
if (tag == "Mouse")
|
||||
// Cleanup keyboard hook when switching to mouse or expand
|
||||
if (tag == "Mouse" || tag == "Expand")
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
UncheckAllToggleButtons();
|
||||
}
|
||||
|
||||
if (tag == "Expand")
|
||||
{
|
||||
SwitchActionTypeToReplaceWith();
|
||||
HideAppSpecific();
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreNormalActionTypes();
|
||||
ShowAppSpecific();
|
||||
}
|
||||
}
|
||||
|
||||
RaiseValidationStateChanged();
|
||||
}
|
||||
|
||||
private void TriggerKeyToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
@@ -248,6 +285,12 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
string? tag = item.Tag?.ToString();
|
||||
|
||||
// Sync SwitchPresenter with the selected action type
|
||||
if (tag != null && ActionSwitchPresenter != null)
|
||||
{
|
||||
ActionSwitchPresenter.Value = tag;
|
||||
}
|
||||
|
||||
// Cleanup keyboard hook when switching away from key/shortcut
|
||||
if (tag != "KeyOrShortcut")
|
||||
{
|
||||
@@ -671,6 +714,25 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
|
||||
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
|
||||
{
|
||||
// Expand trigger key capture: only accept a single key
|
||||
if (_isCapturingExpandTriggerKey && formattedKeys.Count > 0)
|
||||
{
|
||||
_expandTriggerKey = formattedKeys[0];
|
||||
if (ExpandTriggerKeyVisual != null)
|
||||
{
|
||||
ExpandTriggerKeyVisual.Content = _expandTriggerKey;
|
||||
}
|
||||
|
||||
// Auto-uncheck the toggle after capturing one key
|
||||
if (ExpandTriggerKeyToggleBtn?.IsChecked == true)
|
||||
{
|
||||
ExpandTriggerKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
RaiseValidationStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentInputMode == KeyInputMode.OriginalKeys)
|
||||
{
|
||||
_triggerKeys.Clear();
|
||||
@@ -807,7 +869,14 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// </summary>
|
||||
public bool IsInputComplete()
|
||||
{
|
||||
// Trigger keys are always required
|
||||
// Expand trigger: abbreviation + expanded text required (no trigger keys needed)
|
||||
if (CurrentTriggerType == TriggerType.Expand)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(ExpandAbbreviationBox?.Text)
|
||||
&& !string.IsNullOrWhiteSpace(TextContentBox?.Text);
|
||||
}
|
||||
|
||||
// Trigger keys are always required for other trigger types
|
||||
if (_triggerKeys.Count == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -870,6 +939,12 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
// ReplaceWith is handled by SwitchActionTypeToReplaceWith(), not by index
|
||||
if (actionType == ActionType.ReplaceWith)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string tag = actionType switch
|
||||
{
|
||||
ActionType.Text => "Text",
|
||||
@@ -1009,6 +1084,11 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
ActionKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
if (ExpandTriggerKeyToggleBtn?.IsChecked == true)
|
||||
{
|
||||
ExpandTriggerKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetDropDownsEnabled(ItemsControl itemsControl, bool enabled)
|
||||
@@ -1089,6 +1169,21 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case ActionType.ReplaceWith:
|
||||
if (ExpandAbbreviationBox != null && _expandAbbreviationDirty && string.IsNullOrWhiteSpace(ExpandAbbreviationBox.Text))
|
||||
{
|
||||
ShowValidationErrorFromType(ValidationErrorType.EmptyExpandAbbreviation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextContentBox != null && _textContentDirty && string.IsNullOrWhiteSpace(TextContentBox.Text))
|
||||
{
|
||||
ShowValidationErrorFromType(ValidationErrorType.EmptyExpandedText);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1111,6 +1206,24 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
_textContentDirty = false;
|
||||
_urlPathDirty = false;
|
||||
_programPathDirty = false;
|
||||
_expandAbbreviationDirty = false;
|
||||
|
||||
// Reset expand state
|
||||
_expandTriggerKey = "Space";
|
||||
_isCapturingExpandTriggerKey = false;
|
||||
|
||||
if (ExpandAbbreviationBox != null)
|
||||
{
|
||||
ExpandAbbreviationBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
if (ExpandTriggerKeyVisual != null)
|
||||
{
|
||||
ExpandTriggerKeyVisual.Content = "Space";
|
||||
}
|
||||
|
||||
// Restore normal action types if currently in Expand mode
|
||||
RestoreNormalActionTypes();
|
||||
|
||||
// Hide any validation messages
|
||||
HideValidationMessage();
|
||||
@@ -1168,6 +1281,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
AppSpecificCheckBox.IsChecked = false;
|
||||
AppSpecificCheckBox.IsEnabled = false;
|
||||
AppSpecificCheckBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// Reset app combo boxes
|
||||
@@ -1287,6 +1401,167 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
AllowChords = AllowChordsCheckBox.IsChecked == true;
|
||||
}
|
||||
|
||||
#region Expand Trigger Handling
|
||||
|
||||
private void ExpandAbbreviationBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
UncheckAllToggleButtons();
|
||||
}
|
||||
|
||||
private void ExpandAbbreviationBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
_expandAbbreviationDirty = true;
|
||||
RaiseValidationStateChanged();
|
||||
}
|
||||
|
||||
private void ExpandTriggerKeyToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ExpandTriggerKeyToggleBtn.IsChecked == true)
|
||||
{
|
||||
_isCapturingExpandTriggerKey = true;
|
||||
_currentInputMode = KeyInputMode.OriginalKeys;
|
||||
|
||||
// Uncheck other toggles
|
||||
UncheckAllToggleButtons();
|
||||
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpandTriggerKeyToggleBtn_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isCapturingExpandTriggerKey)
|
||||
{
|
||||
_isCapturingExpandTriggerKey = false;
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
}
|
||||
|
||||
private void HideAppSpecific()
|
||||
{
|
||||
if (AppSpecificCheckBox != null)
|
||||
{
|
||||
AppSpecificCheckBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (AppNameTextBox != null)
|
||||
{
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowAppSpecific()
|
||||
{
|
||||
if (AppSpecificCheckBox != null)
|
||||
{
|
||||
AppSpecificCheckBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private void SwitchActionTypeToReplaceWith()
|
||||
{
|
||||
if (ActionTypeComboBox == null || ActionTypeTextItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the "Text" action item and override its label to "Replace with"
|
||||
ActionTypeComboBox.SelectedItem = ActionTypeTextItem;
|
||||
ActionTypeComboBox.IsEnabled = false;
|
||||
|
||||
if (ActionTypeTextLabel != null)
|
||||
{
|
||||
_savedTextLabel = ActionTypeTextLabel.Text;
|
||||
ActionTypeTextLabel.Text = "Replace with";
|
||||
}
|
||||
|
||||
if (ActionTypeTextIcon != null)
|
||||
{
|
||||
_savedTextIconGlyph = ActionTypeTextIcon.Glyph;
|
||||
ActionTypeTextIcon.Glyph = "\uE8C8";
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreNormalActionTypes()
|
||||
{
|
||||
if (ActionTypeComboBox == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore original label and icon on the "Text" action item
|
||||
if (ActionTypeTextLabel != null && _savedTextLabel != null)
|
||||
{
|
||||
ActionTypeTextLabel.Text = _savedTextLabel;
|
||||
_savedTextLabel = null;
|
||||
}
|
||||
|
||||
if (ActionTypeTextIcon != null && _savedTextIconGlyph != null)
|
||||
{
|
||||
ActionTypeTextIcon.Glyph = _savedTextIconGlyph;
|
||||
_savedTextIconGlyph = null;
|
||||
}
|
||||
|
||||
ActionTypeComboBox.SelectedIndex = 0;
|
||||
ActionTypeComboBox.IsEnabled = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Expand Public API - Getters
|
||||
|
||||
public string GetExpandAbbreviation() => ExpandAbbreviationBox?.Text ?? string.Empty;
|
||||
|
||||
public string GetExpandTriggerKey() => _expandTriggerKey;
|
||||
|
||||
public string GetExpandedText() => TextContentBox?.Text ?? string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Expand Public API - Setters
|
||||
|
||||
public void SetExpandAbbreviation(string abbreviation)
|
||||
{
|
||||
if (ExpandAbbreviationBox != null)
|
||||
{
|
||||
ExpandAbbreviationBox.Text = abbreviation;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetExpandTriggerKey(string triggerKey)
|
||||
{
|
||||
_expandTriggerKey = triggerKey;
|
||||
if (ExpandTriggerKeyVisual != null)
|
||||
{
|
||||
ExpandTriggerKeyVisual.Content = triggerKey;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetExpandedText(string text)
|
||||
{
|
||||
if (TextContentBox != null)
|
||||
{
|
||||
TextContentBox.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTriggerType(TriggerType triggerType)
|
||||
{
|
||||
int index = triggerType switch
|
||||
{
|
||||
TriggerType.Expand => 1,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if (TriggerTypeComboBox != null)
|
||||
{
|
||||
TriggerTypeComboBox.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
Shortcut,
|
||||
MouseClick,
|
||||
Url,
|
||||
Expand,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public class ExpandMapping : IToggleableShortcut
|
||||
{
|
||||
public List<string> Shortcut { get; set; } = new List<string>();
|
||||
|
||||
public string Abbreviation { get; set; } = string.Empty;
|
||||
|
||||
public string TriggerKey { get; set; } = "Space";
|
||||
|
||||
public string ExpandedText { get; set; } = string.Empty;
|
||||
|
||||
public bool IsAllApps { get; set; } = true;
|
||||
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,8 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
EmptyUrl,
|
||||
EmptyProgramPath,
|
||||
OneKeyMapping,
|
||||
EmptyExpandAbbreviation,
|
||||
EmptyExpandedText,
|
||||
DuplicateExpandAbbreviation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
{ ValidationErrorType.EmptyUrl, (ResourceHelper.GetString("Validation_EmptyUrl_Title"), ResourceHelper.GetString("Validation_EmptyUrl_Message")) },
|
||||
{ ValidationErrorType.EmptyProgramPath, (ResourceHelper.GetString("Validation_EmptyProgramPath_Title"), ResourceHelper.GetString("Validation_EmptyProgramPath_Message")) },
|
||||
{ ValidationErrorType.OneKeyMapping, (ResourceHelper.GetString("Validation_OneKeyMapping_Title"), ResourceHelper.GetString("Validation_OneKeyMapping_Message")) },
|
||||
{ ValidationErrorType.EmptyExpandAbbreviation, (ResourceHelper.GetString("Validation_EmptyExpandAbbreviation_Title"), ResourceHelper.GetString("Validation_EmptyExpandAbbreviation_Message")) },
|
||||
{ ValidationErrorType.EmptyExpandedText, (ResourceHelper.GetString("Validation_EmptyExpandedText_Title"), ResourceHelper.GetString("Validation_EmptyExpandedText_Message")) },
|
||||
{ ValidationErrorType.DuplicateExpandAbbreviation, (ResourceHelper.GetString("Validation_DuplicateExpandAbbreviation_Title"), ResourceHelper.GetString("Validation_DuplicateExpandAbbreviation_Message")) },
|
||||
};
|
||||
|
||||
public static ValidationErrorType ValidateKeyMapping(
|
||||
@@ -261,6 +264,34 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ValidationErrorType ValidateExpandMapping(
|
||||
string abbreviation,
|
||||
string expandedText,
|
||||
List<ExpandMapping> existingMappings,
|
||||
bool isEditMode = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(abbreviation))
|
||||
{
|
||||
return ValidationErrorType.EmptyExpandAbbreviation;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(expandedText))
|
||||
{
|
||||
return ValidationErrorType.EmptyExpandedText;
|
||||
}
|
||||
|
||||
int upperLimit = isEditMode ? 1 : 0;
|
||||
int duplicateCount = existingMappings.Count(m =>
|
||||
string.Equals(m.Abbreviation, abbreviation, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (duplicateCount > upperLimit)
|
||||
{
|
||||
return ValidationErrorType.DuplicateExpandAbbreviation;
|
||||
}
|
||||
|
||||
return ValidationErrorType.NoError;
|
||||
}
|
||||
|
||||
private static ValidationErrorType ValidateProgramOrUrlMapping(
|
||||
List<string> originalKeys,
|
||||
bool isAppSpecific,
|
||||
|
||||
@@ -85,6 +85,22 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
int ifRunningAction = 0,
|
||||
int visibility = 0);
|
||||
|
||||
[DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool AddExpandMapping(
|
||||
IntPtr config,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string abbreviation,
|
||||
int triggerKey,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string expandedText,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string targetApp);
|
||||
|
||||
[DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DeleteExpandMapping(
|
||||
IntPtr config,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string abbreviation,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string targetApp);
|
||||
|
||||
// Delete Mapping Functions
|
||||
[DllImport(DllName, CallingConvention = Convention)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
|
||||
@@ -253,6 +253,16 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
(int)shortcutKeyMapping.OperationType);
|
||||
}
|
||||
|
||||
public bool AddExpandMapping(string abbreviation, int triggerKey, string expandedText, string targetApp)
|
||||
{
|
||||
return KeyboardManagerInterop.AddExpandMapping(_configHandle, abbreviation, triggerKey, expandedText, targetApp);
|
||||
}
|
||||
|
||||
public bool DeleteExpandMapping(string abbreviation, string targetApp)
|
||||
{
|
||||
return KeyboardManagerInterop.DeleteExpandMapping(_configHandle, abbreviation, targetApp);
|
||||
}
|
||||
|
||||
public bool SaveSettings()
|
||||
{
|
||||
return KeyboardManagerInterop.SaveMappingSettings(_configHandle);
|
||||
|
||||
@@ -16,5 +16,6 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
RunProgram = 1,
|
||||
OpenUri = 2,
|
||||
RemapText = 3,
|
||||
ExpandText = 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,6 +540,88 @@
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Expand Section -->
|
||||
<StackPanel Orientation="Vertical" Visibility="{x:Bind ExpandMappings.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
<TextBlock
|
||||
x:Uid="ExpandMappingsHeader"
|
||||
Margin="16,16,0,8"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<Rectangle
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<ListView
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ExpandMappingsList_ItemClick"
|
||||
ItemsSource="{x:Bind ExpandMappings}"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="helper:ExpandMapping">
|
||||
<Grid MinHeight="48" Padding="0,8,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle Style="{StaticResource ItemDividerStyle}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<controls:IconLabelControl ActionType="Text" Label="{x:Bind Abbreviation}" />
|
||||
<TextBlock
|
||||
x:Uid="ExpandsToText"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<controls:IconLabelControl
|
||||
ActionType="Expand"
|
||||
Label="{x:Bind ExpandedText}"
|
||||
Style="{StaticResource RemappedIconLabelControlStyle}" />
|
||||
<TextBlock
|
||||
x:Uid="ExpandTriggerKeyText"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<commoncontrols:KeyVisual Content="{x:Bind TriggerKey}" Style="{StaticResource OriginalKeyVisualStyle}" />
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
Visibility="{x:Bind AppName, Converter={StaticResource StringVisibilityConverter}}">
|
||||
<TextBlock
|
||||
x:Uid="InText"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<controls:IconLabelControl ActionType="Program" Label="{x:Bind AppName}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<ToggleSwitch
|
||||
IsOn="{x:Bind IsActive}"
|
||||
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
|
||||
Toggled="ToggleSwitch_Toggled" />
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=14}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="DeleteMenuItem"
|
||||
Click="DeleteMapping_Click"
|
||||
Icon="{ui:FontIcon Glyph=,
|
||||
FontSize=14}"
|
||||
Tag="{x:Bind}" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Disabled Section -->
|
||||
<StackPanel Orientation="Vertical" Visibility="{x:Bind DisabledList.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
<TextBlock
|
||||
|
||||
@@ -81,6 +81,8 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
|
||||
public ObservableCollection<URLShortcut> UrlShortcuts { get; } = new();
|
||||
|
||||
public ObservableCollection<ExpandMapping> ExpandMappings { get; } = new();
|
||||
|
||||
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
|
||||
private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
|
||||
|
||||
@@ -92,6 +94,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
TextMapping,
|
||||
ProgramShortcut,
|
||||
UrlShortcut,
|
||||
ExpandMapping,
|
||||
}
|
||||
|
||||
public ItemType Type { get; set; }
|
||||
@@ -305,6 +308,33 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
await ShowRemappingDialog();
|
||||
}
|
||||
|
||||
private async void ExpandMappingsList_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is not ExpandMapping expandMapping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isEditMode = true;
|
||||
_editingItem = new EditingItem
|
||||
{
|
||||
Type = EditingItem.ItemType.ExpandMapping,
|
||||
Item = expandMapping,
|
||||
OriginalTriggerKeys = new List<string>(),
|
||||
AppName = expandMapping.AppName,
|
||||
IsAllApps = expandMapping.IsAllApps,
|
||||
};
|
||||
|
||||
UnifiedMappingControl.Reset();
|
||||
UnifiedMappingControl.SetTriggerType(UnifiedMappingControl.TriggerType.Expand);
|
||||
UnifiedMappingControl.SetExpandAbbreviation(expandMapping.Abbreviation);
|
||||
UnifiedMappingControl.SetExpandTriggerKey(expandMapping.TriggerKey);
|
||||
UnifiedMappingControl.SetExpandedText(expandMapping.ExpandedText);
|
||||
UnifiedMappingControl.SetAppSpecific(!expandMapping.IsAllApps, expandMapping.AppName);
|
||||
RemappingDialog.Title = "Edit remapping";
|
||||
await ShowRemappingDialog();
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task ShowRemappingDialog()
|
||||
{
|
||||
RemappingDialog.PrimaryButtonClick += RemappingDialog_PrimaryButtonClick;
|
||||
@@ -367,7 +397,10 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
{
|
||||
List<string> triggerKeys = UnifiedMappingControl.GetTriggerKeys();
|
||||
|
||||
if (triggerKeys == null || triggerKeys.Count == 0)
|
||||
// Expand trigger type does not use traditional trigger keys
|
||||
bool isExpandMode = UnifiedMappingControl.CurrentTriggerType == UnifiedMappingControl.TriggerType.Expand;
|
||||
|
||||
if (!isExpandMode && (triggerKeys == null || triggerKeys.Count == 0))
|
||||
{
|
||||
UnifiedMappingControl.ShowValidationErrorFromType(ValidationErrorType.EmptyOriginalKeys);
|
||||
args.Cancel = true;
|
||||
@@ -394,6 +427,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.ActionType.OpenUrl => SaveUrlMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.OpenApp => SaveProgramMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.Disable => SaveDisableMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.ReplaceWith => SaveExpandMapping(),
|
||||
UnifiedMappingControl.ActionType.MouseClick => throw new NotImplementedException("Mouse click remapping is not yet supported."),
|
||||
_ => false,
|
||||
};
|
||||
@@ -439,6 +473,8 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
triggerKeys, UnifiedMappingControl.GetProgramPath(), isAppSpecific, appName, _mappingService!, _isEditMode),
|
||||
UnifiedMappingControl.ActionType.Disable => ValidationHelper.ValidateDisableMapping(
|
||||
triggerKeys, isAppSpecific, appName, _mappingService!, _isEditMode, editingRemapping),
|
||||
UnifiedMappingControl.ActionType.ReplaceWith => ValidationHelper.ValidateExpandMapping(
|
||||
UnifiedMappingControl.GetExpandAbbreviation(), UnifiedMappingControl.GetExpandedText(), ExpandMappings.ToList(), _isEditMode),
|
||||
_ => ValidationErrorType.NoError,
|
||||
};
|
||||
}
|
||||
@@ -683,6 +719,44 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
return saved;
|
||||
}
|
||||
|
||||
private bool SaveExpandMapping()
|
||||
{
|
||||
string abbreviation = UnifiedMappingControl.GetExpandAbbreviation();
|
||||
string triggerKey = UnifiedMappingControl.GetExpandTriggerKey();
|
||||
string expandedText = UnifiedMappingControl.GetExpandedText();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(abbreviation) || string.IsNullOrWhiteSpace(expandedText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string targetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty;
|
||||
|
||||
var shortcutKeyMapping = new ShortcutKeyMapping
|
||||
{
|
||||
OperationType = ShortcutOperationType.ExpandText,
|
||||
OriginalKeys = abbreviation,
|
||||
TargetKeys = triggerKey,
|
||||
TargetText = expandedText,
|
||||
TargetApp = targetApp,
|
||||
};
|
||||
|
||||
// Resolve trigger key name to VK code (e.g., "Space" → 0x20)
|
||||
int triggerKeyVk = KeyboardManagerInterop.GetKeyCodeFromName(triggerKey);
|
||||
if (triggerKeyVk == 0)
|
||||
{
|
||||
triggerKeyVk = 0x20; // Default to VK_SPACE
|
||||
}
|
||||
|
||||
// Persist to engine config (default.json) via C++ interop
|
||||
_mappingService?.AddExpandMapping(abbreviation, triggerKeyVk, expandedText, targetApp);
|
||||
_mappingService?.SaveSettings();
|
||||
|
||||
// Also persist to editor settings for UI state
|
||||
SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete Handlers
|
||||
@@ -739,6 +813,19 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
|
||||
private void HandleShortcutDelete(IToggleableShortcut shortcut)
|
||||
{
|
||||
if (shortcut is ExpandMapping)
|
||||
{
|
||||
ShortcutKeyMapping skm = SettingsManager.EditorSettings.ShortcutSettingsDictionary[shortcut.Id].Shortcut;
|
||||
if (shortcut.IsActive)
|
||||
{
|
||||
_mappingService!.DeleteExpandMapping(skm.OriginalKeys, skm.TargetApp);
|
||||
_mappingService.SaveSettings();
|
||||
}
|
||||
|
||||
SettingsManager.RemoveShortcutKeyMappingFromSettings(shortcut.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
bool deleted = shortcut.Shortcut.Count == 1
|
||||
? DeleteSingleKeyToTextMapping(shortcut.Shortcut[0]) // Remapping has its own handler, single key will always be text mapping
|
||||
: DeleteMultiKeyShortcut(shortcut);
|
||||
@@ -804,14 +891,34 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
if (shortcut is ExpandMapping expandMapping)
|
||||
{
|
||||
ShortcutKeyMapping skm = SettingsManager.EditorSettings.ShortcutSettingsDictionary[shortcut.Id].Shortcut;
|
||||
int triggerKeyVk = KeyboardManagerInterop.GetKeyCodeFromName(skm.TargetKeys);
|
||||
if (triggerKeyVk == 0)
|
||||
{
|
||||
triggerKeyVk = 0x20;
|
||||
}
|
||||
|
||||
bool saved = _mappingService!.AddExpandMapping(skm.OriginalKeys, triggerKeyVk, skm.TargetText, skm.TargetApp);
|
||||
if (saved)
|
||||
{
|
||||
shortcut.IsActive = true;
|
||||
SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
|
||||
_mappingService.SaveSettings();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutKeyMapping shortcutKeyMapping = SettingsManager.EditorSettings.ShortcutSettingsDictionary[shortcut.Id].Shortcut;
|
||||
bool saved = shortcut.Shortcut.Count == 1
|
||||
bool saved2 = shortcut.Shortcut.Count == 1
|
||||
? _mappingService!.AddSingleKeyToTextMapping(_mappingService.GetKeyCodeFromName(shortcut.Shortcut[0]), shortcutKeyMapping.TargetText)
|
||||
: shortcutKeyMapping.OperationType == ShortcutOperationType.RemapText
|
||||
? _mappingService!.AddShortcutMapping(shortcutKeyMapping.OriginalKeys, shortcutKeyMapping.TargetText, operationType: ShortcutOperationType.RemapText)
|
||||
: _mappingService!.AddShortcutMapping(shortcutKeyMapping);
|
||||
|
||||
if (saved)
|
||||
if (saved2)
|
||||
{
|
||||
shortcut.IsActive = true;
|
||||
SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
|
||||
@@ -829,11 +936,25 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
bool deleted = shortcut.Shortcut.Count == 1
|
||||
if (shortcut is ExpandMapping expandMapping)
|
||||
{
|
||||
ShortcutKeyMapping skm = SettingsManager.EditorSettings.ShortcutSettingsDictionary[shortcut.Id].Shortcut;
|
||||
bool deleted = _mappingService!.DeleteExpandMapping(skm.OriginalKeys, skm.TargetApp);
|
||||
if (deleted)
|
||||
{
|
||||
shortcut.IsActive = false;
|
||||
SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
|
||||
_mappingService.SaveSettings();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool deleted2 = shortcut.Shortcut.Count == 1
|
||||
? DeleteSingleKeyToTextMapping(shortcut.Shortcut[0])
|
||||
: DeleteMultiKeyMapping(shortcut.Shortcut, shortcut.AppName);
|
||||
|
||||
if (deleted)
|
||||
if (deleted2)
|
||||
{
|
||||
shortcut.IsActive = false;
|
||||
SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
|
||||
@@ -882,12 +1003,13 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
LoadTextMappings();
|
||||
LoadProgramShortcuts();
|
||||
LoadUrlShortcuts();
|
||||
LoadExpandMappings();
|
||||
UpdateHasAnyMappings();
|
||||
}
|
||||
|
||||
private void UpdateHasAnyMappings()
|
||||
{
|
||||
bool hasAny = RemappingList.Count > 0 || DisabledList.Count > 0 || TextMappings.Count > 0 || ProgramShortcuts.Count > 0 || UrlShortcuts.Count > 0;
|
||||
bool hasAny = RemappingList.Count > 0 || DisabledList.Count > 0 || TextMappings.Count > 0 || ProgramShortcuts.Count > 0 || UrlShortcuts.Count > 0 || ExpandMappings.Count > 0;
|
||||
MappingState = hasAny ? "HasMappings" : "Empty";
|
||||
}
|
||||
|
||||
@@ -1024,6 +1146,35 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadExpandMappings()
|
||||
{
|
||||
SettingsManager.EditorSettings.ShortcutsByOperationType.TryGetValue(ShortcutOperationType.ExpandText, out var expandIds);
|
||||
|
||||
if (expandIds == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ExpandMappings.Clear();
|
||||
|
||||
foreach (var id in expandIds)
|
||||
{
|
||||
ShortcutSettings shortcutSettings = SettingsManager.EditorSettings.ShortcutSettingsDictionary[id];
|
||||
ShortcutKeyMapping mapping = shortcutSettings.Shortcut;
|
||||
|
||||
ExpandMappings.Add(new ExpandMapping
|
||||
{
|
||||
Abbreviation = mapping.OriginalKeys,
|
||||
TriggerKey = mapping.TargetKeys,
|
||||
ExpandedText = mapping.TargetText,
|
||||
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
|
||||
AppName = mapping.TargetApp ?? string.Empty,
|
||||
Id = shortcutSettings.Id,
|
||||
IsActive = shortcutSettings.IsActive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> ParseKeyCodes(string keyCodesString)
|
||||
{
|
||||
return keyCodesString.Split(';')
|
||||
|
||||
@@ -378,6 +378,42 @@
|
||||
<data name="MouseClickPlaceholder.Text" xml:space="preserve">
|
||||
<value>Mouse click action - coming soon</value>
|
||||
</data>
|
||||
<data name="TriggerType_Expand.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Text expand</value>
|
||||
</data>
|
||||
<data name="TriggerType_Expand_Text.Text" xml:space="preserve">
|
||||
<value>Text expand</value>
|
||||
</data>
|
||||
<data name="ExpandAbbreviationBox.Header" xml:space="preserve">
|
||||
<value>Abbreviation</value>
|
||||
</data>
|
||||
<data name="ExpandAbbreviationBox.PlaceholderText" xml:space="preserve">
|
||||
<value>e.g. brb</value>
|
||||
</data>
|
||||
<data name="ExpandTriggerKeyLabel.Text" xml:space="preserve">
|
||||
<value>Trigger key</value>
|
||||
</data>
|
||||
<data name="ActionType_ReplaceWith.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Replace with</value>
|
||||
</data>
|
||||
<data name="ActionType_ReplaceWith_Text.Text" xml:space="preserve">
|
||||
<value>Replace with</value>
|
||||
</data>
|
||||
<data name="ExpandedTextBox.Header" xml:space="preserve">
|
||||
<value>Expanded text</value>
|
||||
</data>
|
||||
<data name="ExpandedTextBox.PlaceholderText" xml:space="preserve">
|
||||
<value>e.g. be right back</value>
|
||||
</data>
|
||||
<data name="ExpandMappingsHeader.Text" xml:space="preserve">
|
||||
<value>Text expansions</value>
|
||||
</data>
|
||||
<data name="ExpandsToText.Text" xml:space="preserve">
|
||||
<value>expands to</value>
|
||||
</data>
|
||||
<data name="ExpandTriggerKeyText.Text" xml:space="preserve">
|
||||
<value>via</value>
|
||||
</data>
|
||||
<data name="Validation_EmptyOriginalKeys_Title" xml:space="preserve">
|
||||
<value>Missing original keys</value>
|
||||
</data>
|
||||
@@ -450,6 +486,24 @@
|
||||
<data name="Validation_OneKeyMapping_Message" xml:space="preserve">
|
||||
<value>A single key cannot be remapped to a Program or URL shortcut. Please choose a combination of keys.</value>
|
||||
</data>
|
||||
<data name="Validation_EmptyExpandAbbreviation_Title" xml:space="preserve">
|
||||
<value>Missing abbreviation</value>
|
||||
</data>
|
||||
<data name="Validation_EmptyExpandAbbreviation_Message" xml:space="preserve">
|
||||
<value>Please enter the abbreviation text that will trigger the expansion.</value>
|
||||
</data>
|
||||
<data name="Validation_EmptyExpandedText_Title" xml:space="preserve">
|
||||
<value>Missing expanded text</value>
|
||||
</data>
|
||||
<data name="Validation_EmptyExpandedText_Message" xml:space="preserve">
|
||||
<value>Please enter the text that the abbreviation will expand to.</value>
|
||||
</data>
|
||||
<data name="Validation_DuplicateExpandAbbreviation_Title" xml:space="preserve">
|
||||
<value>Duplicate abbreviation</value>
|
||||
</data>
|
||||
<data name="Validation_DuplicateExpandAbbreviation_Message" xml:space="preserve">
|
||||
<value>This abbreviation is already used by another text expansion.</value>
|
||||
</data>
|
||||
<data name="Warning_ShortcutStartWithModifier" xml:space="preserve">
|
||||
<value>Shortcuts must start with a modifier key (Ctrl, Alt, Shift, or Win).</value>
|
||||
</data>
|
||||
@@ -501,4 +555,4 @@
|
||||
<data name="CheckServiceBtn.Content" xml:space="preserve">
|
||||
<value>Check service status</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <common/utils/gpo.h>
|
||||
#include <keyboardmanager/common/KeyboardManagerConstants.h>
|
||||
#include <keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.h>
|
||||
#include <keyboardmanager/KeyboardManagerEngineLibrary/Tsf4TextReplacer.h>
|
||||
#include <keyboardmanager/KeyboardManagerEngineLibrary/trace.h>
|
||||
#include <common/interop/shared_constants.h>
|
||||
|
||||
@@ -73,6 +74,9 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/,
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize TSF4 text input provider on the main thread (before the message loop).
|
||||
Tsf4TextReplacer::Initialize();
|
||||
|
||||
auto kbm = KeyboardManager();
|
||||
if (kbm.HasRegisteredRemappings())
|
||||
kbm.StartLowlevelKeyboardHook();
|
||||
@@ -84,6 +88,7 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/,
|
||||
run_message_loop({}, {}, { { KeyboardManager::StartHookMessageID, StartHookFunc } });
|
||||
|
||||
kbm.StopLowlevelKeyboardHook();
|
||||
Tsf4TextReplacer::Shutdown();
|
||||
Trace::UnregisterProvider();
|
||||
|
||||
trace.Flush();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <keyboardmanager/common/InputInterface.h>
|
||||
#include <keyboardmanager/common/Helpers.h>
|
||||
#include <keyboardmanager/KeyboardManagerEngineLibrary/trace.h>
|
||||
#include "Tsf4TextReplacer.h"
|
||||
|
||||
#include <TlHelp32.h>
|
||||
#include <thread>
|
||||
@@ -1803,4 +1804,63 @@ namespace KeyboardEventHandlers
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Function to handle text expansion (abbreviation → expanded text) via TSF4.
|
||||
// When the trigger key is pressed, reads the last N characters from the focused
|
||||
// text box. If they match an abbreviation, replaces them with the expanded text.
|
||||
intptr_t HandleExpandTextEvent(LowlevelKeyboardEvent* data, State& state)
|
||||
{
|
||||
if (GeneratedByKBM(data))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (data->wParam != WM_KEYDOWN)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!Tsf4TextReplacer::IsAvailable())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD vkCode = data->lParam->vkCode;
|
||||
|
||||
// Check if this key is a trigger key for any expand mapping.
|
||||
// Also filter by the current application if app-specific.
|
||||
std::wstring currentApp;
|
||||
bool currentAppResolved = false;
|
||||
|
||||
for (const auto& mapping : state.expandMappings)
|
||||
{
|
||||
if (mapping.triggerKey != vkCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check app-specific filter
|
||||
if (!mapping.appName.empty())
|
||||
{
|
||||
if (!currentAppResolved)
|
||||
{
|
||||
currentApp = Helpers::GetCurrentApplication(false);
|
||||
currentAppResolved = true;
|
||||
}
|
||||
|
||||
// Case-insensitive comparison
|
||||
if (_wcsicmp(currentApp.c_str(), mapping.appName.c_str()) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (Tsf4TextReplacer::TryExpand(mapping.abbreviation, mapping.expandedText))
|
||||
{
|
||||
return 1; // Swallow the trigger key
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ namespace KeyboardEventHandlers
|
||||
// Function to generate a unicode string in response to a single keypress
|
||||
intptr_t HandleSingleKeyToTextRemapEvent(KeyboardManagerInput::InputInterface& ii, LowlevelKeyboardEvent* data, State& state);
|
||||
|
||||
// Function to handle text expansion (abbreviation → expanded text) via TSF4
|
||||
intptr_t HandleExpandTextEvent(LowlevelKeyboardEvent* data, State& state);
|
||||
|
||||
// Function to ensure Ctrl/Shift/Alt modifier key state is not detected as pressed down by applications which detect keys at a lower level than hooks when it is remapped for scenarios where its required
|
||||
void ResetIfModifierKeyForLowerLevelKeyHandlers(KeyboardManagerInput::InputInterface& ii, DWORD key, DWORD target);
|
||||
};
|
||||
|
||||
@@ -181,7 +181,7 @@ bool KeyboardManager::HasRegisteredRemappings() const
|
||||
|
||||
bool KeyboardManager::HasRegisteredRemappingsUnchecked() const
|
||||
{
|
||||
return !(state.appSpecificShortcutReMap.empty() && state.appSpecificShortcutReMapSortedKeys.empty() && state.osLevelShortcutReMap.empty() && state.osLevelShortcutReMapSortedKeys.empty() && state.singleKeyReMap.empty() && state.singleKeyToTextReMap.empty());
|
||||
return !(state.appSpecificShortcutReMap.empty() && state.appSpecificShortcutReMapSortedKeys.empty() && state.osLevelShortcutReMap.empty() && state.osLevelShortcutReMapSortedKeys.empty() && state.singleKeyReMap.empty() && state.singleKeyToTextReMap.empty() && state.expandMappings.empty());
|
||||
}
|
||||
|
||||
intptr_t KeyboardManager::HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) noexcept
|
||||
@@ -233,6 +233,14 @@ intptr_t KeyboardManager::HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) n
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Handle text expansion (abbreviation + trigger key → expanded text via TSF4)
|
||||
intptr_t ExpandTextResult = KeyboardEventHandlers::HandleExpandTextEvent(data, state);
|
||||
|
||||
if (ExpandTextResult == 1)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Handle an os-level shortcut remapping
|
||||
return KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent(inputHandler, data, state);
|
||||
}
|
||||
|
||||
@@ -38,10 +38,12 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="State.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="Tsf4TextReplacer.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="KeyboardEventHandlers.cpp" />
|
||||
<ClCompile Include="KeyboardManager.cpp" />
|
||||
<ClCompile Include="Tsf4TextReplacer.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
@@ -56,6 +58,14 @@
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<!-- TSF4 (Text Services Framework 4) WinMD for C++/WinRT projection generation -->
|
||||
<ItemGroup>
|
||||
<Reference Include="Windows.UI.Input.Preview.Text.PreviewTextContract">
|
||||
<HintPath>$(WindowsSdkDir)References\10.0.26100.0\Windows.UI.Input.Preview.Text.PreviewTextContract\1.0.0.0\Windows.UI.Input.Preview.Text.PreviewTextContract.winmd</HintPath>
|
||||
<IsWinMDFile>true</IsWinMDFile>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="$(RepoRoot)deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "pch.h"
|
||||
#include "Tsf4TextReplacer.h"
|
||||
|
||||
#include <winrt/Windows.ApplicationModel.h>
|
||||
#include <winrt/Windows.UI.Input.Preview.Text.h>
|
||||
#include <winrt/Windows.UI.Text.Core.h>
|
||||
|
||||
using namespace winrt::Windows::UI::Input::Preview::Text;
|
||||
using namespace winrt::Windows::UI::Text::Core;
|
||||
|
||||
namespace
|
||||
{
|
||||
TextInputProvider s_provider{ nullptr };
|
||||
bool s_initialized = false;
|
||||
bool s_available = false;
|
||||
|
||||
// LAF token computed for PFN: Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe
|
||||
constexpr wchar_t LafFeatureId[] = L"com.microsoft.windows.textinputmethod";
|
||||
constexpr wchar_t LafToken[] = L"fs0FTtO4rVEbMtnhuNmCNA==";
|
||||
constexpr wchar_t LafPublisherId[] = L"8wekyb3d8bbwe";
|
||||
|
||||
void UnlockTextInputMethodFeature() noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
std::wstring attestation = std::wstring(LafPublisherId) + L" has registered their use of " + LafFeatureId + L" with Microsoft and agrees to the terms of use.";
|
||||
|
||||
auto result = winrt::Windows::ApplicationModel::LimitedAccessFeatures::TryUnlockFeature(
|
||||
LafFeatureId, LafToken, attestation);
|
||||
|
||||
Logger::info(L"TSF4 LAF unlock status: {}", static_cast<int>(result.Status()));
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::warn(L"TSF4 LAF unlock failed: {}", ex.message().c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn(L"TSF4 LAF unlock failed (no package identity?)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Tsf4TextReplacer
|
||||
{
|
||||
void Initialize() noexcept
|
||||
{
|
||||
if (s_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_initialized = true;
|
||||
|
||||
UnlockTextInputMethodFeature();
|
||||
|
||||
try
|
||||
{
|
||||
auto service = TextInputService::GetForCurrentThread();
|
||||
if (!service)
|
||||
{
|
||||
Logger::warn(L"TSF4: TextInputService not available on this thread");
|
||||
return;
|
||||
}
|
||||
|
||||
s_provider = service.CreateTextInputProvider(L"");
|
||||
if (!s_provider)
|
||||
{
|
||||
Logger::warn(L"TSF4: Failed to create TextInputProvider");
|
||||
return;
|
||||
}
|
||||
|
||||
TextInputServiceSubscription subscription{};
|
||||
subscription.requiredEnabledFeatures = TextBoxFeatures::None;
|
||||
s_provider.SetSubscription(subscription);
|
||||
|
||||
s_available = true;
|
||||
Logger::info(L"TSF4: Text input provider initialized successfully");
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::warn(L"TSF4: Initialization failed: {}", ex.message().c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn(L"TSF4: Initialization failed with unknown exception");
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAvailable() noexcept
|
||||
{
|
||||
return s_available && s_provider;
|
||||
}
|
||||
|
||||
bool TryExpand(const std::wstring& abbreviation, const std::wstring& expandedText) noexcept
|
||||
{
|
||||
if (!s_available || !s_provider)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!s_provider.HasFocusedTextBox())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto session = s_provider.CreateEditSession();
|
||||
if (!session)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int textLength = session.TextLength();
|
||||
int abbrevLen = static_cast<int>(abbreviation.length());
|
||||
|
||||
if (textLength < abbrevLen)
|
||||
{
|
||||
session.SubmitPayload();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the last N characters (where N = abbreviation length)
|
||||
CoreTextRange range{};
|
||||
range.StartCaretPosition = textLength - abbrevLen;
|
||||
range.EndCaretPosition = textLength;
|
||||
|
||||
winrt::hstring tail = session.GetText(range);
|
||||
|
||||
// Case-insensitive comparison
|
||||
if (_wcsicmp(tail.c_str(), abbreviation.c_str()) == 0)
|
||||
{
|
||||
session.ReplaceText(range, expandedText);
|
||||
session.SubmitPayload();
|
||||
return true;
|
||||
}
|
||||
|
||||
session.SubmitPayload();
|
||||
return false;
|
||||
}
|
||||
catch (const winrt::hresult_error&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Shutdown() noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
s_provider = nullptr;
|
||||
s_available = false;
|
||||
s_initialized = false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// TSF4 (Text Services Framework 4) text expansion utilities.
|
||||
// Uses Windows.UI.Input.Preview.Text APIs to read/replace text in the
|
||||
// currently focused text box for the Expand (abbreviation expansion) feature.
|
||||
|
||||
namespace Tsf4TextReplacer
|
||||
{
|
||||
// Initialize the TSF4 provider on the current thread.
|
||||
// Must be called on a thread that has a message pump (typically the main thread).
|
||||
void Initialize() noexcept;
|
||||
|
||||
// Whether TSF4 was initialized and is available.
|
||||
bool IsAvailable() noexcept;
|
||||
|
||||
// Try to expand an abbreviation in the focused text box.
|
||||
// Reads the last |abbreviation.length()| characters before the cursor.
|
||||
// If they match |abbreviation| (case-insensitive), replaces them with |expandedText|.
|
||||
// Returns true if expansion occurred, false otherwise.
|
||||
bool TryExpand(const std::wstring& abbreviation, const std::wstring& expandedText) noexcept;
|
||||
|
||||
// Release TSF4 resources. Called on shutdown.
|
||||
void Shutdown() noexcept;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <filesystem>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <common/logger/logger.h>
|
||||
@@ -76,6 +76,15 @@ namespace KeyboardManagerConstants
|
||||
// Name of the property use to store the target application.
|
||||
inline const std::wstring TargetAppSettingName = L"targetApp";
|
||||
|
||||
// Name of the property used to store expand (abbreviation) mappings.
|
||||
inline const std::wstring ExpandMappingsSettingName = L"expandMappings";
|
||||
|
||||
// Name of the property used to store the trigger key for expand mappings.
|
||||
inline const std::wstring ExpandTriggerKeySettingName = L"triggerKey";
|
||||
|
||||
// Name of the property used to store the expanded text for expand mappings.
|
||||
inline const std::wstring ExpandedTextSettingName = L"expandedText";
|
||||
|
||||
// Name of the default configuration.
|
||||
inline const std::wstring DefaultConfiguration = L"default";
|
||||
|
||||
|
||||
@@ -407,6 +407,60 @@ bool MappingConfiguration::LoadShortcutRemaps(const json::JsonObject& jsonData,
|
||||
return result;
|
||||
}
|
||||
|
||||
bool MappingConfiguration::LoadExpandMappings(const json::JsonObject& jsonData)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
try
|
||||
{
|
||||
expandMappings.clear();
|
||||
|
||||
if (!jsonData.HasKey(KeyboardManagerConstants::ExpandMappingsSettingName))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
auto expandArray = jsonData.GetNamedArray(KeyboardManagerConstants::ExpandMappingsSettingName);
|
||||
for (const auto& it : expandArray)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto obj = it.GetObjectW();
|
||||
ExpandMapping mapping;
|
||||
mapping.abbreviation = obj.GetNamedString(KeyboardManagerConstants::OriginalKeysSettingName).c_str();
|
||||
mapping.expandedText = obj.GetNamedString(KeyboardManagerConstants::ExpandedTextSettingName).c_str();
|
||||
|
||||
if (obj.HasKey(KeyboardManagerConstants::ExpandTriggerKeySettingName))
|
||||
{
|
||||
mapping.triggerKey = static_cast<DWORD>(obj.GetNamedNumber(KeyboardManagerConstants::ExpandTriggerKeySettingName));
|
||||
}
|
||||
|
||||
if (obj.HasKey(KeyboardManagerConstants::TargetAppSettingName))
|
||||
{
|
||||
mapping.appName = obj.GetNamedString(KeyboardManagerConstants::TargetAppSettingName).c_str();
|
||||
}
|
||||
|
||||
if (!mapping.abbreviation.empty() && !mapping.expandedText.empty())
|
||||
{
|
||||
expandMappings.push_back(std::move(mapping));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Improper expand mapping JSON. Try the next mapping.");
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Improper JSON format for expand mappings. Skip.");
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool MappingConfiguration::LoadSettings()
|
||||
{
|
||||
Logger::trace(L"SettingsHelper::LoadSettings()");
|
||||
@@ -435,6 +489,7 @@ bool MappingConfiguration::LoadSettings()
|
||||
result = LoadShortcutRemaps(*configFile, KeyboardManagerConstants::RemapShortcutsSettingName) && result;
|
||||
result = LoadShortcutRemaps(*configFile, KeyboardManagerConstants::RemapShortcutsToTextSettingName) && result;
|
||||
result = LoadSingleKeyToTextRemaps(*configFile) && result;
|
||||
result = LoadExpandMappings(*configFile) && result;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -632,10 +687,25 @@ bool MappingConfiguration::SaveSettingsToFile()
|
||||
|
||||
remapKeys.SetNamedValue(KeyboardManagerConstants::InProcessRemapKeysSettingName, inProcessRemapKeysArray);
|
||||
remapKeysToText.SetNamedValue(KeyboardManagerConstants::InProcessRemapKeysSettingName, inProcessRemapKeysToTextArray);
|
||||
json::JsonArray expandMappingsArray;
|
||||
for (const auto& em : expandMappings)
|
||||
{
|
||||
json::JsonObject obj;
|
||||
obj.SetNamedValue(KeyboardManagerConstants::OriginalKeysSettingName, json::value(em.abbreviation));
|
||||
obj.SetNamedValue(KeyboardManagerConstants::ExpandedTextSettingName, json::value(em.expandedText));
|
||||
obj.SetNamedValue(KeyboardManagerConstants::ExpandTriggerKeySettingName, json::JsonValue::CreateNumberValue(static_cast<double>(em.triggerKey)));
|
||||
if (!em.appName.empty())
|
||||
{
|
||||
obj.SetNamedValue(KeyboardManagerConstants::TargetAppSettingName, json::value(em.appName));
|
||||
}
|
||||
expandMappingsArray.Append(obj);
|
||||
}
|
||||
|
||||
configJson.SetNamedValue(KeyboardManagerConstants::RemapKeysSettingName, remapKeys);
|
||||
configJson.SetNamedValue(KeyboardManagerConstants::RemapKeysToTextSettingName, remapKeysToText);
|
||||
configJson.SetNamedValue(KeyboardManagerConstants::RemapShortcutsSettingName, remapShortcuts);
|
||||
configJson.SetNamedValue(KeyboardManagerConstants::RemapShortcutsToTextSettingName, remapShortcutsToText);
|
||||
configJson.SetNamedValue(KeyboardManagerConstants::ExpandMappingsSettingName, expandMappingsArray);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@ using SingleKeyToTextRemapTable = SingleKeyRemapTable;
|
||||
using ShortcutRemapTable = std::map<Shortcut, RemapShortcut>;
|
||||
using AppSpecificShortcutRemapTable = std::map<std::wstring, ShortcutRemapTable>;
|
||||
|
||||
// A single expand mapping: abbreviation + trigger key → expanded text
|
||||
struct ExpandMapping
|
||||
{
|
||||
std::wstring abbreviation;
|
||||
DWORD triggerKey = VK_SPACE; // Virtual key code of the trigger key
|
||||
std::wstring expandedText;
|
||||
std::wstring appName; // Empty = all apps
|
||||
};
|
||||
|
||||
using ExpandMappingTable = std::vector<ExpandMapping>;
|
||||
|
||||
class MappingConfiguration
|
||||
{
|
||||
public:
|
||||
@@ -66,6 +77,9 @@ public:
|
||||
AppSpecificShortcutRemapTable appSpecificShortcutReMap;
|
||||
std::map<std::wstring, std::vector<Shortcut>> appSpecificShortcutReMapSortedKeys;
|
||||
|
||||
// Stores expand (abbreviation → text) mappings
|
||||
ExpandMappingTable expandMappings;
|
||||
|
||||
// Stores the current configuration name.
|
||||
std::wstring currentConfig = KeyboardManagerConstants::DefaultConfiguration;
|
||||
|
||||
@@ -74,4 +88,5 @@ private:
|
||||
bool LoadSingleKeyToTextRemaps(const json::JsonObject& jsonData);
|
||||
bool LoadShortcutRemaps(const json::JsonObject& jsonData, const std::wstring& objectName);
|
||||
bool LoadAppSpecificShortcutRemaps(const json::JsonObject& remapShortcutsData);
|
||||
bool LoadExpandMappings(const json::JsonObject& jsonData);
|
||||
};
|
||||
@@ -269,6 +269,52 @@
|
||||
</controls:SettingsGroup>
|
||||
</tkcontrols:Case>
|
||||
</tkcontrols:SwitchPresenter>
|
||||
|
||||
<!-- TSF4 (Text Expand) API support status -->
|
||||
<controls:SettingsGroup x:Uid="KeyboardManager_Tsf4Status" IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="KeyboardManager_Tsf4Api"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<!-- Supported -->
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||
Glyph=""
|
||||
Visibility="{x:Bind ViewModel.IsTsf4Supported, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock
|
||||
x:Uid="KeyboardManager_Tsf4Api_Supported"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{x:Bind ViewModel.IsTsf4Supported, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<!-- Not supported: info button with flyout + update button -->
|
||||
<Button
|
||||
x:Uid="KeyboardManager_Tsf4Api_InfoButton"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.IsTsf4Supported, Converter={StaticResource ReverseBoolToVisibilityConverter}}">
|
||||
<Button.Flyout>
|
||||
<Flyout ShouldConstrainToRootBounds="False">
|
||||
<StackPanel Width="360" Spacing="12">
|
||||
<TextBlock
|
||||
x:Uid="KeyboardManager_Tsf4Api_Unsupported_Title"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Uid="KeyboardManager_Tsf4Api_Unsupported_Description"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
<Button
|
||||
x:Uid="KeyboardManager_Tsf4Api_Update"
|
||||
Command="{x:Bind ViewModel.OpenWindowsUpdateCommand}"
|
||||
Visibility="{x:Bind ViewModel.IsTsf4Supported, Converter={StaticResource ReverseBoolToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
|
||||
@@ -566,6 +566,38 @@ opera.exe</value>
|
||||
<value>Keys</value>
|
||||
<comment>Keyboard Manager remap keyboard header</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Status.Header" xml:space="preserve">
|
||||
<value>Text Expand</value>
|
||||
<comment>Keyboard Manager TSF4 status section header</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api.Header" xml:space="preserve">
|
||||
<value>Text Services Framework 4 (TSF4) API</value>
|
||||
<comment>TSF4 API availability status card header</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api.Description" xml:space="preserve">
|
||||
<value>Text Expand requires TSF4 API support from the operating system</value>
|
||||
<comment>TSF4 API availability status card description</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api_Supported.Text" xml:space="preserve">
|
||||
<value>Supported</value>
|
||||
<comment>TSF4 API is supported on this OS version</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api_InfoButton.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>View requirements</value>
|
||||
<comment>Tooltip for the TSF4 unsupported info button</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api_Unsupported_Title.Text" xml:space="preserve">
|
||||
<value>Windows update required</value>
|
||||
<comment>Title shown in TSF4 unsupported flyout</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api_Unsupported_Description.Text" xml:space="preserve">
|
||||
<value>Text Expand requires Windows 11 build 20099 or higher. This update may not yet be available on your device due to your update rollout schedule.</value>
|
||||
<comment>Description shown in TSF4 unsupported flyout explaining the OS requirement</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_Tsf4Api_Update.Content" xml:space="preserve">
|
||||
<value>Update Windows</value>
|
||||
<comment>Button to open Windows Update when TSF4 is not supported</comment>
|
||||
</data>
|
||||
<data name="KeyboardManager_RemapShortcutsButton.Header" xml:space="preserve">
|
||||
<value>Remap a shortcut</value>
|
||||
<comment>Keyboard Manager remap shortcuts button</comment>
|
||||
|
||||
@@ -280,6 +280,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsTsf4Supported => OSVersionHelper.IsTsf4Supported();
|
||||
|
||||
public ICommand OpenWindowsUpdateCommand => new RelayCommand(() =>
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("ms-settings:windowsupdate") { UseShellExecute = true });
|
||||
});
|
||||
|
||||
public ICommand RemapKeyboardCommand => _remapKeyboardCommand ?? (_remapKeyboardCommand = new RelayCommand(OnRemapKeyboard));
|
||||
|
||||
public ICommand EditShortcutCommand => _editShortcutCommand ?? (_editShortcutCommand = new RelayCommand(OnEditShortcut));
|
||||
|
||||
Reference in New Issue
Block a user