mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 19:09:43 +02:00
Compare commits
16 Commits
user/muyua
...
yuleng/wor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab99b6faa5 | ||
|
|
3197ae1221 | ||
|
|
b7556c835f | ||
|
|
d6319516d0 | ||
|
|
53737cbe31 | ||
|
|
bf6ff579d3 | ||
|
|
0afe525f31 | ||
|
|
a43fb12d6f | ||
|
|
bc56443443 | ||
|
|
3298625b67 | ||
|
|
ae9f241ef1 | ||
|
|
67a9fa2d13 | ||
|
|
1cfc923bdb | ||
|
|
2dd802f367 | ||
|
|
a0d17406ba | ||
|
|
4a27c5d5f9 |
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -1234,6 +1234,8 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
Nouveaut
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: wpf-to-winui3-migration
|
||||
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
|
||||
description: 'Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.'
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<PackageVersion Include="Mages" Version="3.0.0" />
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
@@ -64,7 +64,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
@@ -76,7 +76,7 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
|
||||
@@ -151,4 +151,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -39,6 +39,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natstepfilter" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -119,7 +119,7 @@
|
||||
</resheader>
|
||||
<data name="context_menu_item_new" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="context_menu_item_open_templates" xml:space="preserve">
|
||||
<value>Open templates</value>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</resheader>
|
||||
<data name="context_menu_item_new" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="context_menu_item_open_templates" xml:space="preserve">
|
||||
<value>Open templates</value>
|
||||
|
||||
@@ -171,6 +171,7 @@ FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
|
||||
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,230,26
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,34,230,18
|
||||
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
|
||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
|
||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,230,10
|
||||
@@ -182,8 +183,6 @@ BEGIN
|
||||
LTEXT "4.0",IDC_STATIC,190,136,12,8
|
||||
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
|
||||
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,230,17
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,230,18
|
||||
END
|
||||
|
||||
DRAW DIALOGEX 0, 0, 260, 228
|
||||
@@ -315,26 +314,31 @@ BEGIN
|
||||
PUSHBUTTON "Cancel",IDCANCEL,162,142,50,14
|
||||
END
|
||||
|
||||
SNIP DIALOGEX 0, 0, 260, 80
|
||||
SNIP DIALOGEX 0, 0, 272, 105
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file.",IDC_STATIC,7,7,230,19
|
||||
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
|
||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,67,32,80,12
|
||||
LTEXT "Copy text from the selected region to the clipboard:",IDC_STATIC,7,50,230,10
|
||||
LTEXT "Text Toggle:",IDC_STATIC,7,65,55,8
|
||||
CONTROL "",IDC_SNIP_OCR_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,67,63,80,12
|
||||
LTEXT "Copy a region of the screen to the clipboard, or save it to a file using the save shortcut.",IDC_STATIC,7,7,230,18
|
||||
RTEXT "Snip Toggle:",IDC_STATIC,22,33,45,8
|
||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,71,32,80,12
|
||||
RTEXT "Snip Save Toggle:",IDC_STATIC,7,49,60,8
|
||||
CONTROL "",IDC_SNIP_SAVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,71,48,80,12
|
||||
LTEXT "Copy text from the selected region to the clipboard:",IDC_STATIC,7,66,230,10
|
||||
RTEXT "Text Toggle:",IDC_STATIC,12,82,55,8
|
||||
CONTROL "",IDC_SNIP_OCR_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,71,81,80,12
|
||||
END
|
||||
|
||||
PANORAMA DIALOGEX 0, 0, 260, 105
|
||||
PANORAMA DIALOGEX 0, 0, 260, 140
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
LTEXT "Capture a scrolling panorama of a selected screen region. Select the area, then scroll the content. Move slowly and consistently, and do not rewind to previously covered areas. Press the hotkey again or with Shift to save to a file.",IDC_STATIC,7,7,245,33
|
||||
LTEXT "Panorama Toggle:",IDC_STATIC,7,74,63,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,72,80,12
|
||||
LTEXT "For the best results, scroll slowly and at a constant rate, do not include stationary content (like scrollbars) in the capture area, and avoid content that is changing (e.g., animations or videos). ",IDC_STATIC,7,41,245,30
|
||||
LTEXT "Capture a scrolling panorama of a selected screen region. Select the area, then scroll the content. Move slowly and consistently, and do not rewind to previously covered areas.",IDC_STATIC,7,7,245,30
|
||||
LTEXT "Press the panorama toggle again to copy to the clipboard, or use the save shortcut to save to a file.",IDC_STATIC,7,39,245,18
|
||||
LTEXT "For the best results, scroll slowly and at a constant rate, do not include stationary content (like scrollbars) in the capture area, and avoid content that is changing (e.g., animations or videos). ",IDC_STATIC,7,62,245,30
|
||||
LTEXT "Panorama Toggle:",IDC_STATIC,7,95,80,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,90,93,80,12
|
||||
LTEXT "Panorama Save Toggle:",IDC_STATIC,7,111,80,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_SAVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,90,109,80,12
|
||||
END
|
||||
|
||||
DEMOTYPE DIALOGEX 0, 0, 260, 249
|
||||
@@ -456,7 +460,9 @@ BEGIN
|
||||
"SNIP", DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 265
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 98
|
||||
END
|
||||
|
||||
"PANORAMA", DIALOG
|
||||
|
||||
@@ -17,7 +17,9 @@ DWORD g_BreakToggleKey = ((HOTKEYF_CONTROL) << 8)| '3';
|
||||
DWORD g_DemoTypeToggleKey = ((HOTKEYF_CONTROL) << 8) | '7';
|
||||
DWORD g_RecordToggleKey = ((HOTKEYF_CONTROL) << 8) | '5';
|
||||
DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
|
||||
DWORD g_SnipSaveToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_SHIFT) << 8) | '6';
|
||||
DWORD g_SnipPanoramaToggleKey = ((HOTKEYF_CONTROL) << 8) | '8';
|
||||
DWORD g_SnipPanoramaSaveToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_SHIFT) << 8) | '8';
|
||||
DWORD g_SnipOcrToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_ALT) << 8) | '6';
|
||||
|
||||
DWORD g_ShowExpiredTime = 1;
|
||||
@@ -80,7 +82,9 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"DrawToggleKey", SETTING_TYPE_DWORD, 0, &g_DrawToggleKey, static_cast<DOUBLE>(g_DrawToggleKey) },
|
||||
{ L"RecordToggleKey", SETTING_TYPE_DWORD, 0, &g_RecordToggleKey, static_cast<DOUBLE>(g_RecordToggleKey) },
|
||||
{ L"SnipToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipToggleKey, static_cast<DOUBLE>(g_SnipToggleKey) },
|
||||
{ L"SnipSaveToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipSaveToggleKey, static_cast<DOUBLE>(g_SnipSaveToggleKey) },
|
||||
{ L"SnipPanoramaToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaToggleKey, static_cast<DOUBLE>(g_SnipPanoramaToggleKey) },
|
||||
{ L"SnipPanoramaSaveToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaSaveToggleKey, static_cast<DOUBLE>(g_SnipPanoramaSaveToggleKey) },
|
||||
{ L"SnipOcrToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipOcrToggleKey, static_cast<DOUBLE>(g_SnipOcrToggleKey) },
|
||||
{ L"PenColor", SETTING_TYPE_DWORD, 0, &g_PenColor, static_cast<DOUBLE>(g_PenColor) },
|
||||
{ L"PenWidth", SETTING_TYPE_DWORD, 0, &g_RootPenWidth, static_cast<DOUBLE>(g_RootPenWidth) },
|
||||
|
||||
@@ -174,6 +174,8 @@ DWORD g_RecordToggleMod;
|
||||
DWORD g_SnipToggleMod;
|
||||
DWORD g_SnipPanoramaToggleMod;
|
||||
DWORD g_SnipOcrToggleMod;
|
||||
DWORD g_SnipSaveToggleMod;
|
||||
DWORD g_SnipPanoramaSaveToggleMod;
|
||||
|
||||
BOOLEAN g_ZoomOnLiveZoom = FALSE;
|
||||
DWORD g_PenWidth = PEN_WIDTH;
|
||||
@@ -212,7 +214,10 @@ BOOL g_RecordToggle = FALSE;
|
||||
BOOL g_RecordCropping = FALSE;
|
||||
SelectRectangle g_SelectRectangle;
|
||||
WebcamPreviewWindow g_WebcamPreview;
|
||||
// The full path of the last saved recording file.
|
||||
std::wstring g_RecordingSaveLocation;
|
||||
// The last user-chosen recording filename. Used to construct unique recording filenames.
|
||||
std::wstring g_RecordingSaveBaseFilename;
|
||||
std::wstring g_ScreenshotSaveLocation;
|
||||
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
|
||||
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
|
||||
@@ -3582,12 +3587,16 @@ void RegisterAllHotkeys(HWND hWnd)
|
||||
}
|
||||
if (g_SnipToggleKey) {
|
||||
registerHotkey( SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF );
|
||||
registerHotkey( SNIP_SAVE_HOTKEY, ( g_SnipToggleMod ^ MOD_SHIFT ), g_SnipToggleKey & 0xFF );
|
||||
}
|
||||
if( g_SnipPanoramaToggleKey &&
|
||||
if (g_SnipSaveToggleKey) {
|
||||
registerHotkey( SNIP_SAVE_HOTKEY, g_SnipSaveToggleMod, g_SnipSaveToggleKey & 0xFF);
|
||||
}
|
||||
if (g_SnipPanoramaToggleKey &&
|
||||
(g_SnipPanoramaToggleKey != g_SnipToggleKey || g_SnipPanoramaToggleMod != g_SnipToggleMod) ) {
|
||||
registerHotkey( SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF );
|
||||
registerHotkey( SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF );
|
||||
}
|
||||
if (g_SnipPanoramaSaveToggleKey) {
|
||||
registerHotkey( SNIP_PANORAMA_SAVE_HOTKEY, g_SnipPanoramaSaveToggleMod | MOD_NOREPEAT, g_SnipPanoramaSaveToggleKey & 0xFF );
|
||||
}
|
||||
if (g_SnipOcrToggleKey) {
|
||||
registerHotkey( SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF );
|
||||
@@ -4816,6 +4825,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
TCHAR text[32];
|
||||
DWORD newToggleKey, newTimeout, newToggleMod, newBreakToggleKey, newDemoTypeToggleKey, newRecordToggleKey, newSnipToggleKey, newSnipPanoramaToggleKey, newSnipOcrToggleKey;
|
||||
DWORD newDrawToggleKey, newDrawToggleMod, newBreakToggleMod, newDemoTypeToggleMod, newRecordToggleMod, newSnipToggleMod, newSnipPanoramaToggleMod, newSnipOcrToggleMod;
|
||||
DWORD newSnipSaveToggleKey, newSnipSaveToggleMod;
|
||||
DWORD newSnipPanoramaSaveToggleKey, newSnipPanoramaSaveToggleMod;
|
||||
DWORD newLiveZoomToggleKey, newLiveZoomToggleMod;
|
||||
static std::vector<std::pair<std::wstring, std::wstring>> microphones;
|
||||
|
||||
@@ -5050,7 +5061,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
if( g_DemoTypeToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_HOTKEY ), HKM_SETHOTKEY, g_DemoTypeToggleKey, 0 );
|
||||
if( g_RecordToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_HOTKEY), HKM_SETHOTKEY, g_RecordToggleKey, 0 );
|
||||
if( g_SnipToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_HOTKEY), HKM_SETHOTKEY, g_SnipToggleKey, 0 );
|
||||
if( g_SnipSaveToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_SAVE_HOTKEY), HKM_SETHOTKEY, g_SnipSaveToggleKey, 0 );
|
||||
if( g_SnipPanoramaToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_HOTKEY), HKM_SETHOTKEY, g_SnipPanoramaToggleKey, 0 );
|
||||
if( g_SnipPanoramaSaveToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_SAVE_HOTKEY), HKM_SETHOTKEY, g_SnipPanoramaSaveToggleKey, 0 );
|
||||
if( g_SnipOcrToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_OCR_HOTKEY), HKM_SETHOTKEY, g_SnipOcrToggleKey, 0 );
|
||||
CheckDlgButton( hDlg, IDC_SHOW_TRAY_ICON,
|
||||
g_ShowTrayIcon ? BST_CHECKED: BST_UNCHECKED );
|
||||
@@ -5512,7 +5525,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
newDemoTypeToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_HOTKEY ), HKM_GETHOTKEY, 0, 0 ));
|
||||
newRecordToggleKey = static_cast<DWORD>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_HOTKEY), HKM_GETHOTKEY, 0, 0));
|
||||
newSnipToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipSaveToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_SAVE_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipPanoramaToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipPanoramaSaveToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_SAVE_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipOcrToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_OCR_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
|
||||
newToggleMod = GetKeyMod( newToggleKey );
|
||||
@@ -5522,7 +5537,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
newDemoTypeToggleMod = GetKeyMod( newDemoTypeToggleKey );
|
||||
newRecordToggleMod = GetKeyMod(newRecordToggleKey);
|
||||
newSnipToggleMod = GetKeyMod( newSnipToggleKey );
|
||||
newSnipSaveToggleMod = GetKeyMod( newSnipSaveToggleKey );
|
||||
newSnipPanoramaToggleMod = GetKeyMod( newSnipPanoramaToggleKey );
|
||||
newSnipPanoramaSaveToggleMod = GetKeyMod( newSnipPanoramaSaveToggleKey );
|
||||
newSnipOcrToggleMod = GetKeyMod( newSnipOcrToggleKey );
|
||||
|
||||
g_SliderZoomLevel = static_cast<int>(SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_GETPOS, 0, 0 ));
|
||||
@@ -5591,25 +5608,41 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
|
||||
}
|
||||
else if (newSnipToggleKey &&
|
||||
(!RegisterHotKey(GetParent(hDlg), SNIP_HOTKEY, newSnipToggleMod, newSnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_SAVE_HOTKEY, (newSnipToggleMod ^ MOD_SHIFT), newSnipToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_HOTKEY, newSnipToggleMod, newSnipToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipSaveToggleKey &&
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_SAVE_HOTKEY, newSnipSaveToggleMod, newSnipSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified snip save hotkey is already in use.\nSelect a different snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipPanoramaToggleKey &&
|
||||
(newSnipPanoramaToggleKey != newSnipToggleKey || newSnipPanoramaToggleMod != newSnipToggleMod) &&
|
||||
(!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_HOTKEY, newSnipPanoramaToggleMod | MOD_NOREPEAT, newSnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_SAVE_HOTKEY, ( newSnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, newSnipPanoramaToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_HOTKEY, newSnipPanoramaToggleMod | MOD_NOREPEAT, newSnipPanoramaToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipPanoramaSaveToggleKey &&
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_SAVE_HOTKEY, newSnipPanoramaSaveToggleMod | MOD_NOREPEAT, newSnipPanoramaSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified panorama snip save hotkey is already in use.\nSelect a different panorama snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipOcrToggleKey &&
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_OCR_HOTKEY, newSnipOcrToggleMod, newSnipOcrToggleKey & 0xFF)) {
|
||||
@@ -5645,8 +5678,12 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
g_RecordToggleMod = newRecordToggleMod;
|
||||
g_SnipToggleKey = newSnipToggleKey;
|
||||
g_SnipToggleMod = newSnipToggleMod;
|
||||
g_SnipSaveToggleKey = newSnipSaveToggleKey;
|
||||
g_SnipSaveToggleMod = newSnipSaveToggleMod;
|
||||
g_SnipPanoramaToggleKey = newSnipPanoramaToggleKey;
|
||||
g_SnipPanoramaToggleMod = newSnipPanoramaToggleMod;
|
||||
g_SnipPanoramaSaveToggleKey = newSnipPanoramaSaveToggleKey;
|
||||
g_SnipPanoramaSaveToggleMod = newSnipPanoramaSaveToggleMod;
|
||||
g_SnipOcrToggleKey = newSnipOcrToggleKey;
|
||||
g_SnipOcrToggleMod = newSnipOcrToggleMod;
|
||||
reg.WriteRegSettings( RegSettings );
|
||||
@@ -6737,6 +6774,45 @@ void StopRecording()
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// GetTimestampSuffix
|
||||
//
|
||||
// Returns a timestamp string for disambiguating filenames.
|
||||
// Format: " YYYY-MM-DD HHMMSS", e.g." 2025-11-02 143000".
|
||||
//
|
||||
// Used as a suffix for the default recording filename. Ensures
|
||||
// chronological name sorting in Explorer.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
static std::wstring GetTimestampSuffix()
|
||||
{
|
||||
auto const now = std::chrono::system_clock::now();
|
||||
auto const in_time_t = std::chrono::system_clock::to_time_t( now );
|
||||
|
||||
std::tm buf{};
|
||||
localtime_s( &buf, &in_time_t );
|
||||
|
||||
std::wstringstream ss;
|
||||
ss << L" " << std::put_time( &buf, L"%Y-%m-%d %H%M%S" );
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// IsDefaultRecordingFilename
|
||||
//
|
||||
// Determines if the provided filename matches the default recording name.
|
||||
// Case-insensitive comparison.
|
||||
//
|
||||
// Returns:
|
||||
// true if filename is the default; otherwise false.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
static bool IsDefaultRecordingFilename(const std::wstring& filename)
|
||||
{
|
||||
return CompareStringOrdinal( DEFAULT_RECORDING_FILE, -1, filename.c_str(), -1, TRUE ) == CSTR_EQUAL
|
||||
|| CompareStringOrdinal( DEFAULT_GIF_RECORDING_FILE, -1, filename.c_str(), -1, TRUE ) == CSTR_EQUAL;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
@@ -6791,19 +6867,70 @@ std::wstring GetUniqueFilename(const std::wstring& lastSavePath, const wchar_t*
|
||||
//
|
||||
// GetUniqueRecordingFilename
|
||||
//
|
||||
// Gets a unique file name for recording saves, using the " (N)" suffix
|
||||
// approach so that the user can hit OK without worrying about overwriting
|
||||
// if they are making multiple recordings in one session or don't want to
|
||||
// always see an overwrite dialog or stop to clean up files.
|
||||
// Generates a unique filename to be suggested in the "Save As" recording
|
||||
// dialog, based on the user's last chosen filename and save location.
|
||||
// This allows the user to quickly save a recording without worrying about
|
||||
// manual renaming to prevent overwriting earlier recordings.
|
||||
//
|
||||
// There are two distinct behaviors based on the last used filename:
|
||||
//
|
||||
// 1. For the default filename ("Recording.mp4"):
|
||||
// Generates a more descriptive name by appending a timestamp, e.g.
|
||||
// "Recording 2025-11-03 143015.mp4". This ensures chronological sorting
|
||||
// in Explorer when ordered by name and is consistent with other tools.
|
||||
//
|
||||
// 2. For custom filenames (e.g. "Presentation.mp4"):
|
||||
// Appends a numeric suffix if the file already exists, e.g.
|
||||
// "Presentation (1).mp4", "Presentation (2).mp4", etc.
|
||||
//
|
||||
// Returns:
|
||||
// A unique filename (without folder path).
|
||||
//
|
||||
// Relies upon the global state of `g_RecordingSaveLocation` and
|
||||
// `g_RecordingSaveBaseFilename`.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
auto GetUniqueRecordingFilename()
|
||||
static auto GetUniqueRecordingFilename()
|
||||
{
|
||||
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF)
|
||||
? DEFAULT_GIF_RECORDING_FILE
|
||||
: DEFAULT_RECORDING_FILE;
|
||||
|
||||
return GetUniqueFilename(g_RecordingSaveLocation, defaultFile, FOLDERID_Videos);
|
||||
// Without a remembered filename, suggest the default name for the current format.
|
||||
std::wstring baseFilename = g_RecordingSaveBaseFilename.empty()
|
||||
? std::wstring( defaultFile )
|
||||
: g_RecordingSaveBaseFilename;
|
||||
|
||||
std::filesystem::path basePath{ baseFilename };
|
||||
|
||||
// For the default filename, append a timestamp so successive default saves stay
|
||||
// unique and sort chronologically in Explorer.
|
||||
if ( IsDefaultRecordingFilename( basePath.filename().wstring() ) )
|
||||
{
|
||||
return basePath.stem().wstring() + GetTimestampSuffix() + basePath.extension().wstring();
|
||||
}
|
||||
|
||||
// For custom filenames, append a numeric suffix to avoid collisions.
|
||||
std::filesystem::path directory;
|
||||
if ( !g_RecordingSaveLocation.empty() )
|
||||
directory = std::filesystem::path( g_RecordingSaveLocation ).parent_path();
|
||||
if ( directory.empty() )
|
||||
{
|
||||
wil::unique_cotaskmem_string folderPath;
|
||||
if ( SUCCEEDED( SHGetKnownFolderPath( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, folderPath.put() ) ) )
|
||||
directory = folderPath.get();
|
||||
}
|
||||
|
||||
std::wstring baseStem = basePath.stem().wstring();
|
||||
std::wstring baseExtension = basePath.extension().wstring();
|
||||
|
||||
std::filesystem::path testPath = directory / ( baseStem + baseExtension );
|
||||
for ( int index = 1; std::filesystem::exists( testPath ); index++ )
|
||||
{
|
||||
testPath = directory / ( baseStem + L" (" + std::to_wstring( index ) + L')' + baseExtension );
|
||||
}
|
||||
|
||||
return testPath.filename().wstring();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -6835,7 +6962,7 @@ auto GetUniqueScreenshotFilename()
|
||||
//
|
||||
// StartRecordingAsync
|
||||
//
|
||||
// Starts the screen recording.
|
||||
// Initiates screen recording and handles the save dialog workflow.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndRecord ) try
|
||||
@@ -7080,8 +7207,30 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
if (!finalPath.empty())
|
||||
{
|
||||
auto path = std::filesystem::path(finalPath);
|
||||
|
||||
// Remember the user's chosen filename and apply a timestamp to default
|
||||
// names so successive saves stay unique and sort chronologically.
|
||||
std::wstring filename = path.filename().wstring();
|
||||
std::wstring finalFilename = filename;
|
||||
if ( IsDefaultRecordingFilename( filename ) )
|
||||
{
|
||||
// The user accepted or re-typed the default filename. Remember it so the
|
||||
// next suggestion also uses a timestamp, and append one to this save.
|
||||
g_RecordingSaveBaseFilename = filename;
|
||||
finalFilename = path.stem().wstring() + GetTimestampSuffix() + path.extension().wstring();
|
||||
}
|
||||
else if ( CompareStringOrdinal( suggestedName.c_str(), -1, filename.c_str(), -1, TRUE ) != CSTR_EQUAL )
|
||||
{
|
||||
// The user chose their own filename instead of the suggested one. Remember
|
||||
// it so future suggestions use numeric suffixes based on this name.
|
||||
g_RecordingSaveBaseFilename = filename;
|
||||
}
|
||||
|
||||
// The path actually written to disk (with any timestamp applied).
|
||||
std::wstring savedPath = ( path.parent_path() / finalFilename ).wstring();
|
||||
|
||||
winrt::StorageFolder folder{ co_await winrt::StorageFolder::GetFolderFromPathAsync(path.parent_path().c_str()) };
|
||||
destFile = co_await folder.CreateFileAsync(path.filename().c_str(), winrt::CreationCollisionOption::ReplaceExisting);
|
||||
destFile = co_await folder.CreateFileAsync(finalFilename.c_str(), winrt::CreationCollisionOption::ReplaceExisting);
|
||||
|
||||
// If user trimmed, use the trimmed file
|
||||
winrt::StorageFile sourceFile = file;
|
||||
@@ -7099,8 +7248,8 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
try { co_await file.DeleteAsync(); } catch (...) {}
|
||||
}
|
||||
|
||||
// Use finalPath directly - destFile.Path() may be stale after MoveAndReplaceAsync
|
||||
g_RecordingSaveLocation = finalPath;
|
||||
// Use savedPath directly - destFile.Path() may be stale after MoveAndReplaceAsync
|
||||
g_RecordingSaveLocation = savedPath;
|
||||
// Update the registry buffer and save to persist across app restarts
|
||||
wcsncpy_s(g_RecordingSaveLocationBuffer, g_RecordingSaveLocation.c_str(), _TRUNCATE);
|
||||
reg.WriteRegSettings(RegSettings);
|
||||
@@ -7600,7 +7749,9 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_BreakToggleMod = GetKeyMod( g_BreakToggleKey );
|
||||
g_DemoTypeToggleMod = GetKeyMod( g_DemoTypeToggleKey );
|
||||
g_SnipToggleMod = GetKeyMod( g_SnipToggleKey );
|
||||
g_SnipSaveToggleMod = GetKeyMod( g_SnipSaveToggleKey );
|
||||
g_SnipPanoramaToggleMod = GetKeyMod( g_SnipPanoramaToggleKey );
|
||||
g_SnipPanoramaSaveToggleMod = GetKeyMod( g_SnipPanoramaSaveToggleKey );
|
||||
g_SnipOcrToggleMod = GetKeyMod( g_SnipOcrToggleKey );
|
||||
g_RecordToggleMod = GetKeyMod( g_RecordToggleKey );
|
||||
|
||||
@@ -7651,23 +7802,37 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
}
|
||||
else if (g_SnipToggleKey &&
|
||||
(!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipSaveToggleKey &&
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, g_SnipSaveToggleMod, g_SnipSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified snip save hotkey is already in use.\nSelect a different snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipPanoramaToggleKey &&
|
||||
(g_SnipPanoramaToggleKey != g_SnipToggleKey || g_SnipPanoramaToggleMod != g_SnipToggleMod) &&
|
||||
(!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipPanoramaSaveToggleKey &&
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, g_SnipPanoramaSaveToggleMod | MOD_NOREPEAT, g_SnipPanoramaSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified panorama snip save hotkey is already in use.\nSelect a different panorama snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipOcrToggleKey &&
|
||||
!RegisterHotKey(hWnd, SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF)) {
|
||||
@@ -10254,7 +10419,9 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_BreakToggleMod = GetKeyMod(g_BreakToggleKey);
|
||||
g_DemoTypeToggleMod = GetKeyMod(g_DemoTypeToggleKey);
|
||||
g_SnipToggleMod = GetKeyMod(g_SnipToggleKey);
|
||||
g_SnipSaveToggleMod = GetKeyMod(g_SnipSaveToggleKey);
|
||||
g_SnipPanoramaToggleMod = GetKeyMod(g_SnipPanoramaToggleKey);
|
||||
g_SnipPanoramaSaveToggleMod = GetKeyMod(g_SnipPanoramaSaveToggleKey);
|
||||
g_SnipOcrToggleMod = GetKeyMod(g_SnipOcrToggleKey);
|
||||
g_RecordToggleMod = GetKeyMod(g_RecordToggleKey);
|
||||
BOOL showOptions = FALSE;
|
||||
@@ -10317,8 +10484,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
if (g_SnipToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))
|
||||
if (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
@@ -10327,11 +10493,21 @@ LRESULT APIENTRY MainWndProc(
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipSaveToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, g_SnipSaveToggleMod, g_SnipSaveToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified snip save hotkey is already in use.\nSelect a different snip save hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipPanoramaToggleKey &&
|
||||
(g_SnipPanoramaToggleKey != g_SnipToggleKey || g_SnipPanoramaToggleMod != g_SnipToggleMod))
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
@@ -10340,6 +10516,17 @@ LRESULT APIENTRY MainWndProc(
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipPanoramaSaveToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, g_SnipPanoramaSaveToggleMod | MOD_NOREPEAT, g_SnipPanoramaSaveToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified panorama snip save hotkey is already in use.\nSelect a different panorama snip save hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipOcrToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF))
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
#define IDC_WEBCAM_BRIGHTNESS_LABEL 1131
|
||||
#define IDC_WEBCAM_BRIGHTNESS_SLIDER 1132
|
||||
#define IDC_NOISE_CANCELLATION 1133
|
||||
#define IDC_SNIP_SAVE_HOTKEY 1134
|
||||
#define IDC_SNIP_PANORAMA_SAVE_HOTKEY 1135
|
||||
#define IDC_SAVE 40002
|
||||
#define IDC_COPY 40004
|
||||
#define IDC_RECORD 40006
|
||||
@@ -151,8 +153,8 @@
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 120
|
||||
#define _APS_NEXT_COMMAND_VALUE 40015
|
||||
#define _APS_NEXT_CONTROL_VALUE 1134
|
||||
#define _APS_NEXT_COMMAND_VALUE 40012
|
||||
#define _APS_NEXT_CONTROL_VALUE 1136
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -70,8 +70,10 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
{ L"DrawToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"RecordToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipSaveToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipOcrToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipPanoramaToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipPanoramaSaveToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"BreakTimerKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"DemoTypeToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"PenColor", SPECIAL_SEMANTICS_COLOR },
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class ShellArgumentBuilder
|
||||
{
|
||||
public static string BuildArguments(params string[] arguments)
|
||||
{
|
||||
if (arguments.Length <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
AppendArgument(stringBuilder, argument);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendArgument(StringBuilder stringBuilder, string argument)
|
||||
{
|
||||
if (stringBuilder.Length > 0)
|
||||
{
|
||||
stringBuilder.Append(' ');
|
||||
}
|
||||
|
||||
if (argument.Length == 0 || ShouldBeQuoted(argument))
|
||||
{
|
||||
stringBuilder.Append('"');
|
||||
var index = 0;
|
||||
while (index < argument.Length)
|
||||
{
|
||||
var c = argument[index++];
|
||||
if (c == '\\')
|
||||
{
|
||||
var numBackSlash = 1;
|
||||
while (index < argument.Length && argument[index] == '\\')
|
||||
{
|
||||
index++;
|
||||
numBackSlash++;
|
||||
}
|
||||
|
||||
if (index == argument.Length)
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash * 2);
|
||||
}
|
||||
else if (argument[index] == '"')
|
||||
{
|
||||
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
|
||||
stringBuilder.Append('"');
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
{
|
||||
stringBuilder.Append('\\');
|
||||
stringBuilder.Append('"');
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
stringBuilder.Append('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(argument);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldBeQuoted(string argument)
|
||||
{
|
||||
foreach (var c in argument)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '"')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ internal sealed partial class HttpCachingClient : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
_cacheHandler.Dispose();
|
||||
}
|
||||
|
||||
private static bool IsSupportedHttpUri(Uri resourceUri)
|
||||
|
||||
@@ -146,13 +146,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
// The all apps page will kick off a BG thread to start loading apps.
|
||||
// We just want to know when it is done.
|
||||
var allApps = AllAppsCommandProvider.Page;
|
||||
allApps.PropChanged += (s, p) =>
|
||||
{
|
||||
if (p.PropertyName == nameof(allApps.IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
};
|
||||
allApps.PropChanged += AllApps_PropChanged;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
|
||||
@@ -172,6 +166,14 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
private void AllApps_PropChanged(object? sender, IPropChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AllAppsCommandProvider.Page.IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void PinnedCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
_defaultViewDirty = true;
|
||||
@@ -782,6 +784,8 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
|
||||
_tlcManager.PinnedCommands.CollectionChanged -= PinnedCommands_CollectionChanged;
|
||||
|
||||
AllAppsCommandProvider.Page.PropChanged -= AllApps_PropChanged;
|
||||
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settingsService.SettingsChanged -= SettingsChangedHandler;
|
||||
|
||||
@@ -541,6 +541,9 @@ public partial class WinRTExtensionService : IExtensionService, IDisposable
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_catalog.PackageInstalling -= Catalog_PackageInstalling;
|
||||
_catalog.PackageUninstalling -= Catalog_PackageUninstalling;
|
||||
_catalog.PackageUpdating -= Catalog_PackageUpdating;
|
||||
_getInstalledExtensionsLock.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ internal sealed partial class BlurImageControl : Control
|
||||
private SpriteVisual? _effectVisual;
|
||||
private CompositionEffectBrush? _effectBrush;
|
||||
private CompositionSurfaceBrush? _imageBrush;
|
||||
private LoadedImageSurface? _lastLoadedSurface;
|
||||
|
||||
public BlurImageControl()
|
||||
{
|
||||
@@ -379,10 +380,20 @@ internal sealed partial class BlurImageControl : Control
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
|
||||
|
||||
// Each call to LoadImageAsync creates a new LoadedImageSurface backed by native
|
||||
// composition resources. The old surface becomes unrooted once the brush points at
|
||||
// the new one, so it isn't leaked, but dispose it explicitly so the unmanaged
|
||||
// resources are released deterministically instead of waiting for finalization.
|
||||
var previousSurface = _lastLoadedSurface;
|
||||
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
_lastLoadedSurface = loadedSurface;
|
||||
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
|
||||
previousSurface?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,9 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
@@ -22,6 +24,7 @@ public sealed partial class ContentFormControl : UserControl
|
||||
// tree. If this gets GC'ed, then it'll revoke our Action handler, and the
|
||||
// form will do seemingly nothing.
|
||||
private RenderedAdaptiveCard? _renderedCard;
|
||||
private AdaptiveCard? _adaptiveCard;
|
||||
|
||||
public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); }
|
||||
|
||||
@@ -95,9 +98,11 @@ public sealed partial class ContentFormControl : UserControl
|
||||
private void DisplayCard(AdaptiveCardParseResult result)
|
||||
{
|
||||
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
|
||||
_adaptiveCard = result.AdaptiveCard;
|
||||
ContentGrid.Children.Clear();
|
||||
if (_renderedCard.FrameworkElement is not null)
|
||||
{
|
||||
_renderedCard.FrameworkElement.KeyDown += OnFormKeyDown;
|
||||
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
|
||||
|
||||
// Use the Loaded event to ensure we focus after the card is in the visual tree
|
||||
@@ -114,8 +119,9 @@ public sealed partial class ContentFormControl : UserControl
|
||||
|
||||
private void OnFrameworkElementLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
// Only fix once — unhook after first layout pass
|
||||
if (_renderedCard?.FrameworkElement is FrameworkElement element)
|
||||
// Only fix once — unhook from sender (not _renderedCard, which may have been
|
||||
// reassigned by the time this fires).
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
element.LayoutUpdated -= OnFrameworkElementLayoutUpdated;
|
||||
FixToggleAccessibilityNames(element);
|
||||
@@ -276,6 +282,50 @@ public sealed partial class ContentFormControl : UserControl
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnFormKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
// Snapshot the fields so a subsequent DisplayCard call can't swap the
|
||||
// rendered/parsed card out from under us mid-method. This keeps the
|
||||
// resolved submit action and the gathered inputs from the same card.
|
||||
var renderedCard = _renderedCard;
|
||||
var adaptiveCard = _adaptiveCard;
|
||||
|
||||
if (e.Key != VirtualKey.Enter || renderedCard == null || adaptiveCard == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only submit when Enter is pressed inside a single-line TextBox
|
||||
if (e.OriginalSource is TextBox textBox && !textBox.AcceptsReturn)
|
||||
{
|
||||
// Find the first Submit or Execute action on the card
|
||||
IAdaptiveActionElement? submitAction = null;
|
||||
foreach (var action in adaptiveCard.Actions)
|
||||
{
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
{
|
||||
submitAction = action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (submitAction != null)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
// Validate (and gather) the inputs before submitting. AsJson() only
|
||||
// returns the values cached by a successful ValidateInputs() call, so
|
||||
// skipping this would submit an empty payload. This mirrors what the
|
||||
// renderer does internally when a submit button is clicked.
|
||||
var inputs = renderedCard.UserInputs;
|
||||
if (inputs.ValidateInputs(submitAction))
|
||||
{
|
||||
ViewModel?.HandleSubmit(submitAction, inputs.AsJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) =>
|
||||
ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson());
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged += SettingsChangedHandler;
|
||||
|
||||
// Make sure that we update the acrylic theme when the OS theme changes
|
||||
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
|
||||
RootElement.ActualThemeChanged += RootElement_ActualThemeChanged;
|
||||
|
||||
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
|
||||
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
|
||||
@@ -222,6 +222,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
UpdateBackdrop();
|
||||
}
|
||||
|
||||
private void RootElement_ActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(UpdateBackdrop);
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.GoBack)
|
||||
@@ -1683,6 +1688,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged -= SettingsChangedHandler;
|
||||
|
||||
_localKeyboardListener.Dispose();
|
||||
_windowThemeSynchronizer.Dispose();
|
||||
DisposeAcrylic();
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed partial class ExtensionsPage : Page
|
||||
|
||||
private readonly SettingsViewModel? viewModel;
|
||||
private readonly Dictionary<string, WeakReference<SettingsCard>> _vmToCardMap = new();
|
||||
private readonly Dictionary<SettingsCard, ProviderSettingsViewModel> _cardToVmMap = new();
|
||||
|
||||
public ExtensionsPage()
|
||||
{
|
||||
@@ -31,6 +32,23 @@ public sealed partial class ExtensionsPage : Page
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
|
||||
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
|
||||
|
||||
Unloaded += ExtensionsPage_Unloaded;
|
||||
}
|
||||
|
||||
private void ExtensionsPage_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// ProviderSettingsViewModel subscribes to its CommandProviderWrapper (owned by the
|
||||
// singleton TopLevelCommandManager), so a live VM roots this page through the
|
||||
// PropertyChanged handler below. Drain any VMs still hooked when the page is torn
|
||||
// down; SettingsCard_DataContextChanged only unhooks the ones that get recycled.
|
||||
foreach (var vm in _cardToVmMap.Values)
|
||||
{
|
||||
vm.PropertyChanged -= ProviderViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
_cardToVmMap.Clear();
|
||||
_vmToCardMap.Clear();
|
||||
}
|
||||
|
||||
private void SettingsCard_Click(object sender, RoutedEventArgs e)
|
||||
@@ -46,16 +64,28 @@ public sealed partial class ExtensionsPage : Page
|
||||
|
||||
private void SettingsCard_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
|
||||
if (sender is SettingsCard card && card.DataContext is ProviderSettingsViewModel newVm)
|
||||
if (sender is SettingsCard card)
|
||||
{
|
||||
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
|
||||
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
|
||||
|
||||
// Immediately update automation name in case DisplayName is already available
|
||||
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
|
||||
// Unsubscribe from the previous ViewModel to prevent handler accumulation
|
||||
// when virtualization recycles items with a new DataContext.
|
||||
if (_cardToVmMap.TryGetValue(card, out var oldVm))
|
||||
{
|
||||
AutomationProperties.SetName(toggle, newVm.DisplayName);
|
||||
oldVm.PropertyChanged -= ProviderViewModel_PropertyChanged;
|
||||
_cardToVmMap.Remove(card);
|
||||
}
|
||||
|
||||
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
|
||||
if (card.DataContext is ProviderSettingsViewModel newVm)
|
||||
{
|
||||
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
|
||||
_cardToVmMap[card] = newVm;
|
||||
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
|
||||
|
||||
// Immediately update automation name in case DisplayName is already available
|
||||
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
|
||||
{
|
||||
AutomationProperties.SetName(toggle, newVm.DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Helpers;
|
||||
|
||||
[TestClass]
|
||||
public class ShellArgumentBuilderTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("plain", "plain")]
|
||||
[DataRow("C:\\Program Files\\PowerToys", "\"C:\\Program Files\\PowerToys\"")]
|
||||
[DataRow("say \"hello\"", "\"say \\\"hello\\\"\"")]
|
||||
[DataRow("", "\"\"")]
|
||||
[DataRow("C:\\Program Files\\", "\"C:\\Program Files\\\\\"")]
|
||||
public void BuildArguments_FormatsSingleArgument(string argument, string expected)
|
||||
{
|
||||
var actual = ShellArgumentBuilder.BuildArguments(argument);
|
||||
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BuildArguments_FormatsMultipleArguments()
|
||||
{
|
||||
var actual = ShellArgumentBuilder.BuildArguments("plain", "C:\\Program Files\\PowerToys", "two words");
|
||||
|
||||
Assert.AreEqual("plain \"C:\\Program Files\\PowerToys\" \"two words\"", actual);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
|
||||
@@ -24,7 +25,7 @@ internal static class CommandLauncher
|
||||
// You can notice the difference with Recycle Bin for example:
|
||||
// - "explorer ::{645FF040-5081-101B-9F08-00AA002F954E}"
|
||||
// - "::{645FF040-5081-101B-9F08-00AA002F954E}"
|
||||
return ShellHelpers.OpenInShell("explorer.exe", classification.Target);
|
||||
return ShellHelpers.OpenInShell("explorer.exe", ShellArgumentBuilder.BuildArguments(classification.Target));
|
||||
|
||||
case LaunchMethod.ActivateAppId:
|
||||
return ActivateAppId(classification.Target, classification.Arguments);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.Bookmarks.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.Text;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
@@ -42,98 +42,7 @@ public class ShellListPageHelpers
|
||||
executable = segments[0];
|
||||
if (segments.Length > 1)
|
||||
{
|
||||
arguments = ArgumentBuilder.BuildArguments(segments[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArgumentBuilder
|
||||
{
|
||||
internal static string BuildArguments(string[] arguments)
|
||||
{
|
||||
if (arguments.Length <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
AppendArgument(stringBuilder, argument);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendArgument(StringBuilder stringBuilder, string argument)
|
||||
{
|
||||
if (stringBuilder.Length > 0)
|
||||
{
|
||||
stringBuilder.Append(' ');
|
||||
}
|
||||
|
||||
if (argument.Length == 0 || ShouldBeQuoted(argument))
|
||||
{
|
||||
stringBuilder.Append('\"');
|
||||
var index = 0;
|
||||
while (index < argument.Length)
|
||||
{
|
||||
var c = argument[index++];
|
||||
if (c == '\\')
|
||||
{
|
||||
var numBackSlash = 1;
|
||||
while (index < argument.Length && argument[index] == '\\')
|
||||
{
|
||||
index++;
|
||||
numBackSlash++;
|
||||
}
|
||||
|
||||
if (index == argument.Length)
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash * 2);
|
||||
}
|
||||
else if (argument[index] == '\"')
|
||||
{
|
||||
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
|
||||
stringBuilder.Append('\"');
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\"')
|
||||
{
|
||||
stringBuilder.Append('\\');
|
||||
stringBuilder.Append('\"');
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
stringBuilder.Append('\"');
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(argument);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldBeQuoted(string s)
|
||||
{
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '\"')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
arguments = ShellArgumentBuilder.BuildArguments(segments[1..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class ShowFileInFolderCommand : InvokableCommand
|
||||
try
|
||||
{
|
||||
var argument = "/select, \"" + _path + "\"";
|
||||
Process.Start("explorer.exe", argument);
|
||||
using var process = Process.Start("explorer.exe", argument);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -866,6 +866,14 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept
|
||||
std::vector<FancyZonesDataTypes::MonitorId> monitors = { FancyZonesDataTypes::MonitorId{ .monitor = nullptr, .deviceId = { .id = ZonedWindowProperties::MultiMonitorName, .instanceId = ZonedWindowProperties::MultiMonitorInstance } } };
|
||||
if (ShouldWorkAreasBeRecreated(monitors, currentVirtualDesktop, m_workAreaConfiguration.GetAllWorkAreas()))
|
||||
{
|
||||
// WindowMouseSnap caches a raw WorkArea* in m_currentWorkArea and the
|
||||
// WorkArea map by reference. WorkAreaConfiguration::Clear() destroys
|
||||
// every unique_ptr<WorkArea> (and hence the inner ZonesOverlay and
|
||||
// its std::mutex). If a drag is in flight, the next MoveSizeUpdate
|
||||
// would dereference that dangling WorkArea* and lock the freed
|
||||
// mutex. Drain the active drag first so subsequent drag messages
|
||||
// hit the snapper's `if (m_windowMouseSnapper)` guard and no-op.
|
||||
MoveSizeEnd();
|
||||
m_workAreaConfiguration.Clear();
|
||||
|
||||
FancyZonesDataTypes::WorkAreaId workAreaId;
|
||||
@@ -882,6 +890,8 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept
|
||||
|
||||
if (ShouldWorkAreasBeRecreated(monitors, currentVirtualDesktop, workAreas))
|
||||
{
|
||||
// See comment above the matching Clear() in the span-zones branch.
|
||||
MoveSizeEnd();
|
||||
m_workAreaConfiguration.Clear();
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
@@ -1094,6 +1104,9 @@ void FancyZones::SettingsUpdate(SettingId id)
|
||||
break;
|
||||
case SettingId::SpanZonesAcrossMonitors:
|
||||
{
|
||||
// See UpdateWorkAreas() — same WindowMouseSnap dangling-WorkArea*
|
||||
// hazard if the user toggles this setting mid-drag.
|
||||
MoveSizeEnd();
|
||||
m_workAreaConfiguration.Clear();
|
||||
PostMessageW(m_window, WM_PRIV_INIT, NULL, NULL);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,14 @@ void OnThreadExecutor::worker_thread()
|
||||
|
||||
OnThreadExecutor::~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
{
|
||||
// Modify the shared shutdown flag while holding the mutex so the
|
||||
// worker reliably observes it on its next wake. Without this, a notify
|
||||
// racing the worker entering _task_cv.wait can be missed and the join
|
||||
// below hangs forever.
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_shutdown_request = true;
|
||||
}
|
||||
_task_cv.notify_one();
|
||||
_worker_thread.join();
|
||||
}
|
||||
|
||||
@@ -115,6 +115,11 @@ WorkArea::WorkArea(HINSTANCE hinstance, const FancyZonesDataTypes::WorkAreaId& u
|
||||
|
||||
WorkArea::~WorkArea()
|
||||
{
|
||||
// Tear down the renderer (joining its background thread) before returning
|
||||
// the HWND to the pool. Otherwise, the render thread can still be drawing
|
||||
// through m_renderTarget into an HWND that has already been recycled by a
|
||||
// subsequent NewZonesOverlayWindow call.
|
||||
m_zonesOverlay.reset();
|
||||
windowPool.FreeZonesOverlayWindow(m_window);
|
||||
}
|
||||
|
||||
|
||||
@@ -340,13 +340,19 @@ void ZonesOverlay::DrawActiveZoneSet(const ZonesMap& zones,
|
||||
|
||||
ZonesOverlay::~ZonesOverlay()
|
||||
{
|
||||
// Constructor early-returns (e.g. CreateHwndRenderTarget failing during a
|
||||
// display-driver TDR) leave m_renderThread default-constructed; calling
|
||||
// join() on a non-joinable thread terminates the process.
|
||||
if (m_renderThread.joinable())
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_abortThread = true;
|
||||
m_shouldRender = true;
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_abortThread = true;
|
||||
m_shouldRender = true;
|
||||
}
|
||||
m_cv.notify_all();
|
||||
m_renderThread.join();
|
||||
}
|
||||
m_cv.notify_all();
|
||||
m_renderThread.join();
|
||||
|
||||
if (m_renderTarget)
|
||||
{
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
</PropertyGroup>
|
||||
<!-- Props that are constant for both Debug and Release configurations -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<SpectreMitigation>Spectre</SpectreMitigation>
|
||||
@@ -164,7 +163,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets" Condition="Exists('..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
</ImportGroup>
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
@@ -181,7 +180,7 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
</Target>
|
||||
<Target Name="FakeResourcesPriMerge" BeforeTargets="FinalizeBuildStatus" DependsOnTargets="CopyFilesToOutputDirectory">
|
||||
<Message Text="Renaming Microsoft.UI.Xaml.pri to resources.pri" />
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
|
||||
<package id="Microsoft.UI.Xaml" version="2.8.2-prerelease.220830001" targetFramework="native" />
|
||||
<package id="Microsoft.VCRTForwarders.140" version="1.0.7" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.4022.49" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -15,7 +15,6 @@
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
@@ -101,7 +100,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
</ImportGroup>
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
@@ -114,7 +113,7 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
</Target>
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<packages>
|
||||
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
|
||||
<package id="Microsoft.UI.Xaml" version="2.8.2-prerelease.220830001" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.4022.49" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -326,6 +326,7 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
std::wstring origKeysStr = origShortcut.ToHstringVK().c_str();
|
||||
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
|
||||
mapping->targetApp = AllocateAndCopyString(app);
|
||||
mapping->exactMatch = origShortcut.exactMatch ? 1 : 0;
|
||||
|
||||
if (targetShortcutUnion.index() == 0)
|
||||
{
|
||||
@@ -429,6 +430,7 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
|
||||
|
||||
mapping->targetApp = AllocateAndCopyString(app);
|
||||
mapping->exactMatch = origShortcut.exactMatch ? 1 : 0;
|
||||
|
||||
if (targetShortcutUnion.index() == 0)
|
||||
{
|
||||
@@ -533,11 +535,13 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
const wchar_t* startDirectory,
|
||||
int elevation,
|
||||
int ifRunningAction,
|
||||
int visibility)
|
||||
int visibility,
|
||||
int exactMatch)
|
||||
{
|
||||
auto mappingConfig = static_cast<MappingConfiguration*>(config);
|
||||
|
||||
Shortcut originalShortcut(originalKeys);
|
||||
originalShortcut.exactMatch = (exactMatch != 0);
|
||||
|
||||
KeyShortcutTextUnion targetShortcut;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ struct ShortcutMapping
|
||||
wchar_t* programPath;
|
||||
wchar_t* programArgs;
|
||||
wchar_t* uriToOpen;
|
||||
int exactMatch;
|
||||
};
|
||||
|
||||
extern "C"
|
||||
@@ -69,7 +70,8 @@ extern "C"
|
||||
const wchar_t* startDirectory = nullptr,
|
||||
int elevation = 0,
|
||||
int ifRunningAction = 0,
|
||||
int visibility = 0);
|
||||
int visibility = 0,
|
||||
int exactMatch = 0);
|
||||
|
||||
__declspec(dllexport) void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount);
|
||||
__declspec(dllexport) int GetKeyCodeFromName(const wchar_t* keyName);
|
||||
|
||||
@@ -111,6 +111,10 @@
|
||||
x:Uid="AllowChordsCheckBox"
|
||||
Click="AllowChordsCheckBox_Click"
|
||||
IsChecked="True" />
|
||||
<CheckBox
|
||||
x:Name="ExactMatchCheckBox"
|
||||
x:Uid="ExactMatchCheckBox"
|
||||
IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</tkcontrols:Case>
|
||||
<!-- Mouse Button Trigger -->
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
_triggerKeys.CollectionChanged += (_, _) =>
|
||||
{
|
||||
UpdatePlaceholderVisibility();
|
||||
UpdateAppSpecificCheckBoxState();
|
||||
RaiseValidationStateChanged();
|
||||
};
|
||||
|
||||
@@ -561,6 +562,16 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
AppSpecificCheckBox.IsChecked = false;
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// Exact match only applies to shortcuts (multiple keys), matching the classic editor.
|
||||
if (ExactMatchCheckBox != null)
|
||||
{
|
||||
ExactMatchCheckBox.IsEnabled = isShortcut;
|
||||
if (!isShortcut)
|
||||
{
|
||||
ExactMatchCheckBox.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -860,6 +871,23 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the origin shortcut requires an exact match
|
||||
/// (i.e., no extra modifiers may be pressed). Only meaningful for shortcuts.
|
||||
/// </summary>
|
||||
public bool GetExactMatch() => ExactMatchCheckBox?.IsChecked == true && ExactMatchCheckBox?.IsEnabled == true;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the exact-match state for the origin shortcut.
|
||||
/// </summary>
|
||||
public void SetExactMatch(bool value)
|
||||
{
|
||||
if (ExactMatchCheckBox != null)
|
||||
{
|
||||
ExactMatchCheckBox.IsChecked = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the action type.
|
||||
/// </summary>
|
||||
@@ -1170,6 +1198,12 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
AppSpecificCheckBox.IsEnabled = false;
|
||||
}
|
||||
|
||||
if (ExactMatchCheckBox != null)
|
||||
{
|
||||
ExactMatchCheckBox.IsChecked = false;
|
||||
ExactMatchCheckBox.IsEnabled = false;
|
||||
}
|
||||
|
||||
// Reset app combo boxes
|
||||
if (ElevationComboBox != null)
|
||||
{
|
||||
|
||||
@@ -34,5 +34,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
public string IfRunningAction { get; set; } = string.Empty;
|
||||
|
||||
public string Visibility { get; set; } = string.Empty;
|
||||
|
||||
public bool ExactMatch { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
|
||||
public bool ExactMatch { get; set; }
|
||||
|
||||
private bool IsEnabledValue { get; set; } = true;
|
||||
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public static class RemappingHelper
|
||||
{
|
||||
public static bool SaveMapping(KeyboardMappingService mappingService, List<string> originalKeys, List<string> remappedKeys, bool isAppSpecific, string appName, bool saveToSettings = true)
|
||||
public static bool SaveMapping(KeyboardMappingService mappingService, List<string> originalKeys, List<string> remappedKeys, bool isAppSpecific, string appName, bool exactMatch = false, bool saveToSettings = true)
|
||||
{
|
||||
if (mappingService == null)
|
||||
{
|
||||
@@ -76,15 +76,16 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
OriginalKeys = originalKeysString,
|
||||
TargetKeys = targetKeysString,
|
||||
TargetApp = isAppSpecific ? appName : string.Empty,
|
||||
ExactMatch = exactMatch,
|
||||
};
|
||||
|
||||
if (isAppSpecific && !string.IsNullOrEmpty(appName))
|
||||
{
|
||||
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
|
||||
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName, exactMatch: exactMatch);
|
||||
}
|
||||
else
|
||||
{
|
||||
mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
|
||||
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, exactMatch: exactMatch);
|
||||
}
|
||||
|
||||
if (saveToSettings)
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
|
||||
public bool ExactMatch { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
@@ -23,5 +23,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
public bool IsAllApps { get; set; } = true;
|
||||
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
|
||||
public bool ExactMatch { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,9 +248,15 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// Check shortcut mappings
|
||||
// Check shortcut mappings. Only key-output remaps (RemapShortcut) can make a key
|
||||
// reachable again; program/URL/text targets do not restore keyboard input.
|
||||
foreach (var mapping in mappingService.GetShortcutMappings())
|
||||
{
|
||||
if (mapping.OperationType != ShortcutOperationType.RemapShortcut)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] targetKeys = mapping.TargetKeys.Split(';');
|
||||
if (targetKeys.Length == 1 && int.TryParse(targetKeys[0], out int shortcutTargetKey) && shortcutTargetKey == originalKey)
|
||||
{
|
||||
|
||||
@@ -83,7 +83,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? startDirectory = null,
|
||||
int elevation = 0,
|
||||
int ifRunningAction = 0,
|
||||
int visibility = 0);
|
||||
int visibility = 0,
|
||||
int exactMatch = 0);
|
||||
|
||||
// Delete Mapping Functions
|
||||
[DllImport(DllName, CallingConvention = Convention)]
|
||||
@@ -165,6 +166,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
public IntPtr ProgramPath;
|
||||
public IntPtr ProgramArgs;
|
||||
public IntPtr UriToOpen;
|
||||
public int ExactMatch;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
|
||||
@@ -71,6 +71,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
|
||||
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
|
||||
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
|
||||
ExactMatch = mapping.ExactMatch != 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -98,6 +99,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
|
||||
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
|
||||
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
|
||||
ExactMatch = mapping.ExactMatch != 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -192,14 +194,14 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
return KeyboardManagerInterop.AddSingleKeyToTextRemap(_configHandle, originalKey, targetText);
|
||||
}
|
||||
|
||||
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut)
|
||||
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut, bool exactMatch = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalKeys) || string.IsNullOrEmpty(targetKeys))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType);
|
||||
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType, exactMatch: exactMatch ? 1 : 0);
|
||||
}
|
||||
|
||||
public bool AddShortcutMapping(ShortcutKeyMapping shortcutKeyMapping)
|
||||
@@ -232,7 +234,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
string.IsNullOrEmpty(shortcutKeyMapping.StartInDirectory) ? null : shortcutKeyMapping.StartInDirectory,
|
||||
(int)shortcutKeyMapping.Elevation,
|
||||
(int)shortcutKeyMapping.IfRunningAction,
|
||||
(int)shortcutKeyMapping.Visibility);
|
||||
(int)shortcutKeyMapping.Visibility,
|
||||
shortcutKeyMapping.ExactMatch ? 1 : 0);
|
||||
}
|
||||
else if (shortcutKeyMapping.OperationType == ShortcutOperationType.OpenUri)
|
||||
{
|
||||
@@ -242,7 +245,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
shortcutKeyMapping.TargetKeys,
|
||||
shortcutKeyMapping.TargetApp,
|
||||
(int)shortcutKeyMapping.OperationType,
|
||||
shortcutKeyMapping.UriToOpen);
|
||||
shortcutKeyMapping.UriToOpen,
|
||||
exactMatch: shortcutKeyMapping.ExactMatch ? 1 : 0);
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.AddShortcutRemap(
|
||||
@@ -250,7 +254,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
shortcutKeyMapping.OriginalKeys,
|
||||
shortcutKeyMapping.TargetKeys,
|
||||
shortcutKeyMapping.TargetApp,
|
||||
(int)shortcutKeyMapping.OperationType);
|
||||
(int)shortcutKeyMapping.OperationType,
|
||||
exactMatch: shortcutKeyMapping.ExactMatch ? 1 : 0);
|
||||
}
|
||||
|
||||
public bool SaveSettings()
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
|
||||
public string UriToOpen { get; set; } = string.Empty;
|
||||
|
||||
public bool ExactMatch { get; set; }
|
||||
|
||||
public enum ElevationLevel
|
||||
{
|
||||
NonElevated = 0,
|
||||
@@ -79,7 +81,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
Elevation == other.Elevation &&
|
||||
IfRunningAction == other.IfRunningAction &&
|
||||
Visibility == other.Visibility &&
|
||||
UriToOpen == other.UriToOpen;
|
||||
UriToOpen == other.UriToOpen &&
|
||||
ExactMatch == other.ExactMatch;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
@@ -97,6 +100,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
hash.Add(IfRunningAction);
|
||||
hash.Add(Visibility);
|
||||
hash.Add(UriToOpen);
|
||||
hash.Add(ExactMatch);
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,16 +76,25 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Service Down Banner -->
|
||||
<InfoBar
|
||||
x:Name="ServiceDownBanner"
|
||||
x:Uid="ServiceDownBanner"
|
||||
Grid.Row="0"
|
||||
Margin="16,16,16,0"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Error"
|
||||
Visibility="Collapsed" />
|
||||
<StackPanel Grid.Row="0">
|
||||
<!-- Service Down Banner -->
|
||||
<InfoBar
|
||||
x:Name="ServiceDownBanner"
|
||||
x:Uid="ServiceDownBanner"
|
||||
Margin="16,16,16,0"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Error"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<!-- Orphaned-key notice (non-blocking, shown after a single-key remap) -->
|
||||
<InfoBar
|
||||
x:Name="OrphanedKeyBanner"
|
||||
Margin="16,16,16,0"
|
||||
IsClosable="True"
|
||||
IsOpen="False"
|
||||
Severity="Informational" />
|
||||
</StackPanel>
|
||||
|
||||
<ContentControl
|
||||
x:Name="MainContentControl"
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
private bool _disposed;
|
||||
private bool _isEditMode;
|
||||
private EditingItem? _editingItem;
|
||||
private string? _pendingOrphanedKeyName;
|
||||
private string _mappingState = "Empty";
|
||||
private bool _isServiceRunning = true;
|
||||
|
||||
@@ -185,6 +186,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.SetTriggerKeys(remapping.Shortcut.ToList());
|
||||
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.KeyOrShortcut);
|
||||
UnifiedMappingControl.SetActionKeys(remapping.RemappedKeys.ToList());
|
||||
UnifiedMappingControl.SetExactMatch(remapping.ExactMatch);
|
||||
UnifiedMappingControl.SetAppSpecific(!remapping.IsAllApps, remapping.AppName);
|
||||
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
|
||||
await ShowRemappingDialog();
|
||||
@@ -210,6 +212,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.Reset();
|
||||
UnifiedMappingControl.SetTriggerKeys(disabledMapping.Shortcut.ToList());
|
||||
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Disable);
|
||||
UnifiedMappingControl.SetExactMatch(disabledMapping.ExactMatch);
|
||||
UnifiedMappingControl.SetAppSpecific(!disabledMapping.IsAllApps, disabledMapping.AppName);
|
||||
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
|
||||
await ShowRemappingDialog();
|
||||
@@ -236,6 +239,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.SetTriggerKeys(textMapping.Shortcut.ToList());
|
||||
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Text);
|
||||
UnifiedMappingControl.SetTextContent(textMapping.Text);
|
||||
UnifiedMappingControl.SetExactMatch(textMapping.ExactMatch);
|
||||
UnifiedMappingControl.SetAppSpecific(!textMapping.IsAllApps, textMapping.AppName);
|
||||
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
|
||||
await ShowRemappingDialog();
|
||||
@@ -274,6 +278,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.SetIfRunningAction(mapping.IfRunningAction);
|
||||
}
|
||||
|
||||
UnifiedMappingControl.SetExactMatch(programShortcut.ExactMatch);
|
||||
UnifiedMappingControl.SetAppSpecific(!programShortcut.IsAllApps, programShortcut.AppName);
|
||||
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
|
||||
await ShowRemappingDialog();
|
||||
@@ -300,6 +305,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.SetTriggerKeys(urlShortcut.Shortcut.ToList());
|
||||
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenUrl);
|
||||
UnifiedMappingControl.SetUrl(urlShortcut.URL);
|
||||
UnifiedMappingControl.SetExactMatch(urlShortcut.ExactMatch);
|
||||
UnifiedMappingControl.SetAppSpecific(!urlShortcut.IsAllApps, urlShortcut.AppName);
|
||||
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
|
||||
await ShowRemappingDialog();
|
||||
@@ -307,17 +313,32 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
|
||||
private async System.Threading.Tasks.Task ShowRemappingDialog()
|
||||
{
|
||||
_pendingOrphanedKeyName = null;
|
||||
RemappingDialog.PrimaryButtonClick += RemappingDialog_PrimaryButtonClick;
|
||||
UnifiedMappingControl.ValidationStateChanged += UnifiedMappingControl_ValidationStateChanged;
|
||||
RemappingDialog.IsPrimaryButtonEnabled = UnifiedMappingControl.IsInputComplete();
|
||||
|
||||
await RemappingDialog.ShowAsync();
|
||||
ContentDialogResult result = await RemappingDialog.ShowAsync();
|
||||
|
||||
RemappingDialog.PrimaryButtonClick -= RemappingDialog_PrimaryButtonClick;
|
||||
UnifiedMappingControl.ValidationStateChanged -= UnifiedMappingControl_ValidationStateChanged;
|
||||
_isEditMode = false;
|
||||
_editingItem = null;
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
if (result == ContentDialogResult.Primary && _pendingOrphanedKeyName != null)
|
||||
{
|
||||
ShowOrphanedKeyBanner(_pendingOrphanedKeyName);
|
||||
}
|
||||
|
||||
_pendingOrphanedKeyName = null;
|
||||
}
|
||||
|
||||
private void ShowOrphanedKeyBanner(string keyName)
|
||||
{
|
||||
OrphanedKeyBanner.Title = ResourceHelper.GetString("OrphanedKeyInfo_Title");
|
||||
OrphanedKeyBanner.Message = ResourceHelper.GetString("Warning_OrphanedKeys").Replace("{0}", keyName, System.StringComparison.Ordinal);
|
||||
OrphanedKeyBanner.IsOpen = true;
|
||||
}
|
||||
|
||||
private void UnifiedMappingControl_ValidationStateChanged(object? sender, EventArgs e)
|
||||
@@ -382,6 +403,12 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
// If this single-key remap leaves the original key with no assignment, remember it so a
|
||||
// non-blocking notice can be shown after the dialog closes. Saving is not interrupted.
|
||||
_pendingOrphanedKeyName = ShouldWarnOrphanedKeys(UnifiedMappingControl.CurrentActionType, triggerKeys, out string orphanedKeyName)
|
||||
? orphanedKeyName
|
||||
: null;
|
||||
|
||||
if (_isEditMode && _editingItem != null)
|
||||
{
|
||||
DeleteExistingMapping();
|
||||
@@ -404,6 +431,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingOrphanedKeyName = null;
|
||||
UnifiedMappingControl.ShowValidationError(ResourceHelper.GetString("Error_SaveFailed_Title"), ResourceHelper.GetString("Error_SaveFailed_Message"));
|
||||
args.Cancel = true;
|
||||
}
|
||||
@@ -443,6 +471,32 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
};
|
||||
}
|
||||
|
||||
private bool ShouldWarnOrphanedKeys(UnifiedMappingControl.ActionType actionType, List<string> triggerKeys, out string orphanedKeyName)
|
||||
{
|
||||
orphanedKeyName = string.Empty;
|
||||
|
||||
// Orphaned-key detection mirrors the classic Remap Keys editor: it applies only to
|
||||
// single-key remaps, where remapping a key away can leave that physical key unreachable.
|
||||
if (_mappingService == null || triggerKeys.Count != 1 || actionType == UnifiedMappingControl.ActionType.MouseClick)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int originalKey = _mappingService.GetKeyCodeFromName(triggerKeys[0]);
|
||||
if (originalKey == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ValidationHelper.IsKeyOrphaned(originalKey, _mappingService))
|
||||
{
|
||||
orphanedKeyName = triggerKeys[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DeleteExistingMapping()
|
||||
{
|
||||
if (_editingItem == null || _mappingService == null)
|
||||
@@ -508,7 +562,8 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
triggerKeys,
|
||||
actionKeys,
|
||||
UnifiedMappingControl.GetIsAppSpecific(),
|
||||
UnifiedMappingControl.GetAppName());
|
||||
UnifiedMappingControl.GetAppName(),
|
||||
exactMatch: UnifiedMappingControl.GetExactMatch());
|
||||
}
|
||||
|
||||
private bool SaveDisableMapping(List<string> triggerKeys)
|
||||
@@ -526,6 +581,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
OriginalKeys = originalKeysString,
|
||||
TargetKeys = VkDisabledString,
|
||||
TargetApp = isAppSpecific ? appName : string.Empty,
|
||||
ExactMatch = UnifiedMappingControl.GetExactMatch(),
|
||||
};
|
||||
|
||||
if (triggerKeys.Count == 1)
|
||||
@@ -544,7 +600,8 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
_mappingService!.AddShortcutMapping(
|
||||
originalKeysString,
|
||||
VkDisabledString,
|
||||
isAppSpecific ? appName : string.Empty);
|
||||
isAppSpecific ? appName : string.Empty,
|
||||
exactMatch: shortcutKeyMapping.ExactMatch);
|
||||
}
|
||||
|
||||
SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
|
||||
@@ -597,6 +654,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
private bool SaveShortcutToTextMapping(List<string> triggerKeys, string textContent, bool isAppSpecific, string appName)
|
||||
{
|
||||
string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
bool exactMatch = UnifiedMappingControl.GetExactMatch();
|
||||
|
||||
var shortcutKeyMapping = new ShortcutKeyMapping
|
||||
{
|
||||
@@ -605,11 +663,12 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
TargetKeys = textContent,
|
||||
TargetText = textContent,
|
||||
TargetApp = isAppSpecific ? appName : string.Empty,
|
||||
ExactMatch = exactMatch,
|
||||
};
|
||||
|
||||
bool saved = isAppSpecific && !string.IsNullOrEmpty(appName)
|
||||
? _mappingService!.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText)
|
||||
: _mappingService!.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
|
||||
? _mappingService!.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText, exactMatch)
|
||||
: _mappingService!.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText, exactMatch: exactMatch);
|
||||
|
||||
if (saved)
|
||||
{
|
||||
@@ -637,6 +696,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
TargetKeys = originalKeysString,
|
||||
UriToOpen = url,
|
||||
TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
|
||||
ExactMatch = UnifiedMappingControl.GetExactMatch(),
|
||||
};
|
||||
|
||||
bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
|
||||
@@ -671,6 +731,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
Visibility = UnifiedMappingControl.GetVisibility(),
|
||||
Elevation = UnifiedMappingControl.GetElevationLevel(),
|
||||
TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
|
||||
ExactMatch = UnifiedMappingControl.GetExactMatch(),
|
||||
};
|
||||
|
||||
bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
|
||||
@@ -796,7 +857,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
RemappingHelper.SaveMapping(_mappingService!, remapping.Shortcut, remapping.RemappedKeys, !remapping.IsAllApps, remapping.AppName, false);
|
||||
RemappingHelper.SaveMapping(_mappingService!, remapping.Shortcut, remapping.RemappedKeys, !remapping.IsAllApps, remapping.AppName, exactMatch: remapping.ExactMatch, saveToSettings: false);
|
||||
}
|
||||
|
||||
shortcut.IsActive = true;
|
||||
@@ -808,7 +869,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
bool saved = 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.OriginalKeys, shortcutKeyMapping.TargetText, operationType: ShortcutOperationType.RemapText, exactMatch: shortcutKeyMapping.ExactMatch)
|
||||
: _mappingService!.AddShortcutMapping(shortcutKeyMapping);
|
||||
|
||||
if (saved)
|
||||
@@ -860,7 +921,8 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
_mappingService!.AddShortcutMapping(
|
||||
originalKeysString,
|
||||
VkDisabledString,
|
||||
!remapping.IsAllApps ? remapping.AppName : string.Empty);
|
||||
!remapping.IsAllApps ? remapping.AppName : string.Empty,
|
||||
exactMatch: remapping.ExactMatch);
|
||||
}
|
||||
|
||||
_mappingService!.SaveSettings();
|
||||
@@ -919,6 +981,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
AppName = mapping.TargetApp ?? string.Empty,
|
||||
Id = shortcutSettings.Id,
|
||||
IsActive = shortcutSettings.IsActive,
|
||||
ExactMatch = mapping.ExactMatch,
|
||||
};
|
||||
|
||||
if (isDisabled)
|
||||
@@ -957,6 +1020,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
AppName = mapping.TargetApp ?? string.Empty,
|
||||
Id = shortcutSettings.Id,
|
||||
IsActive = shortcutSettings.IsActive,
|
||||
ExactMatch = mapping.ExactMatch,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -991,6 +1055,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
Elevation = mapping.Elevation.ToString(),
|
||||
IfRunningAction = mapping.IfRunningAction.ToString(),
|
||||
Visibility = mapping.Visibility.ToString(),
|
||||
ExactMatch = mapping.ExactMatch,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1020,6 +1085,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
IsActive = shortcutSettings.IsActive,
|
||||
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
|
||||
AppName = mapping.TargetApp ?? string.Empty,
|
||||
ExactMatch = mapping.ExactMatch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +486,14 @@
|
||||
<data name="Warning_Title" xml:space="preserve">
|
||||
<value>Warning</value>
|
||||
</data>
|
||||
<data name="Warning_OrphanedKeys" xml:space="preserve">
|
||||
<value>The key {0} is now remapped, so it can no longer be typed directly unless another key is mapped to it.</value>
|
||||
<comment>{0} is the name of the key that will be left without an assignment.</comment>
|
||||
</data>
|
||||
<data name="OrphanedKeyInfo_Title" xml:space="preserve">
|
||||
<value>A key is no longer directly available</value>
|
||||
<comment>Title of the non-blocking notice shown after remapping a single key away.</comment>
|
||||
</data>
|
||||
<data name="Error_UnknownValidation_Title" xml:space="preserve">
|
||||
<value>Validation error</value>
|
||||
</data>
|
||||
|
||||
@@ -22,6 +22,7 @@ public partial class PowerAccent : IDisposable
|
||||
private const double ScreenMinPadding = 150;
|
||||
|
||||
private bool _visible;
|
||||
private int _showGeneration;
|
||||
private string[] _characters = Array.Empty<string>();
|
||||
private string[] _characterDescriptions = Array.Empty<string>();
|
||||
private int _selectedIndex = -1;
|
||||
@@ -98,6 +99,10 @@ public partial class PowerAccent : IDisposable
|
||||
_initialShiftState = WindowsFunctions.IsShiftState();
|
||||
_visible = true;
|
||||
|
||||
// Each summon gets a generation id so a delayed render queued by an earlier
|
||||
// press can't fire for a newer one (or after the toolbar was hidden).
|
||||
int generation = ++_showGeneration;
|
||||
|
||||
_characters = GetCharacters(letterKey);
|
||||
_characterDescriptions = GetCharacterDescriptions(_characters);
|
||||
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
|
||||
@@ -105,7 +110,7 @@ public partial class PowerAccent : IDisposable
|
||||
Task.Delay(_settingService.InputTime).ContinueWith(
|
||||
t =>
|
||||
{
|
||||
if (_visible)
|
||||
if (_visible && generation == _showGeneration)
|
||||
{
|
||||
OnChangeDisplay?.Invoke(true, _characters);
|
||||
}
|
||||
@@ -237,6 +242,7 @@ public partial class PowerAccent : IDisposable
|
||||
OnChangeDisplay?.Invoke(false, null);
|
||||
_selectedIndex = -1;
|
||||
_visible = false;
|
||||
_showGeneration++;
|
||||
}
|
||||
|
||||
private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
|
||||
|
||||
@@ -28,12 +28,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[CmdConfigureIgnore]
|
||||
public static HotkeySettings DefaultSnipToggleKey => new HotkeySettings(false, true, false, false, '6'); // Ctrl+6
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public static HotkeySettings DefaultSnipSaveToggleKey => new HotkeySettings(false, true, false, true, '6'); // Ctrl+Shift+6
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public static HotkeySettings DefaultSnipOcrToggleKey => new HotkeySettings(false, true, true, false, '6'); // Ctrl+Alt+6
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public static HotkeySettings DefaultSnipPanoramaToggleKey => new HotkeySettings(false, true, false, false, '8'); // Ctrl+8
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public static HotkeySettings DefaultSnipPanoramaSaveToggleKey => new HotkeySettings(false, true, false, true, '8'); // Ctrl+Shift+8
|
||||
|
||||
[CmdConfigureIgnore]
|
||||
public static HotkeySettings DefaultBreakTimerKey => new HotkeySettings(false, true, false, false, '3'); // Ctrl+3
|
||||
|
||||
@@ -50,10 +56,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public KeyboardKeysProperty SnipToggleKey { get; set; }
|
||||
|
||||
public KeyboardKeysProperty SnipSaveToggleKey { get; set; }
|
||||
|
||||
public KeyboardKeysProperty SnipOcrToggleKey { get; set; }
|
||||
|
||||
public KeyboardKeysProperty SnipPanoramaToggleKey { get; set; }
|
||||
|
||||
public KeyboardKeysProperty SnipPanoramaSaveToggleKey { get; set; }
|
||||
|
||||
public KeyboardKeysProperty BreakTimerKey { get; set; }
|
||||
|
||||
public StringProperty Font { get; set; }
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
<controls:PageLink x:Uid="NewPlus_Learn_More" Link="https://aka.ms/PowerToysOverview_NewPlus" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:SettingsPageControl.SecondaryLinks>
|
||||
<controls:PageLink Link="https://www.linkedin.com/in/christian-gaardmark/" Text="Christian Gaardmark" />
|
||||
<controls:PageLink Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt" Text="Based on Christian Gaardmark's New++ from the Productivity Plus Pack" />
|
||||
</controls:SettingsPageControl.SecondaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
|
||||
|
||||
@@ -404,31 +404,26 @@
|
||||
Name="ZoomItSnipShortcut"
|
||||
x:Uid="ZoomIt_Snip_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
<tkcontrols:MarkdownTextBlock Config="{StaticResource DescriptionTextMarkdownConfig}" Text="{x:Bind ViewModel.SnipToggleKeySave, Mode=OneWay, Converter={StaticResource HotkeySettingsToLocalizedStringConverter}, ConverterParameter=ZoomIt_Snip_Shortcut_Save}" />
|
||||
</tkcontrols:SettingsCard.Description>
|
||||
<controls:ShortcutControl HotkeySettings="{x:Bind ViewModel.SnipToggleKey, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="ZoomItSnipSaveShortcut" x:Uid="ZoomIt_SnipSave_Shortcut">
|
||||
<controls:ShortcutControl HotkeySettings="{x:Bind ViewModel.SnipSaveToggleKey, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="ZoomItSnipOcrShortcut"
|
||||
x:Uid="ZoomIt_SnipOcr_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind ViewModel.SnipOcrToggleKey, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsExpander
|
||||
<tkcontrols:SettingsCard
|
||||
Name="ZoomItPanoramaShortcut"
|
||||
x:Uid="ZoomIt_Panorama_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind ViewModel.SnipPanoramaToggleKey, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
<tkcontrols:MarkdownTextBlock Config="{StaticResource DescriptionTextMarkdownConfig}" Text="{x:Bind ViewModel.SnipPanoramaToggleKeySave, Mode=OneWay, Converter={StaticResource HotkeySettingsToLocalizedStringConverter}, ConverterParameter=ZoomIt_Panorama_Shortcut_Save}" />
|
||||
</tkcontrols:SettingsCard.Description>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="ZoomItPanoramaSaveShortcut" x:Uid="ZoomIt_PanoramaSave_Shortcut">
|
||||
<controls:ShortcutControl HotkeySettings="{x:Bind ViewModel.SnipPanoramaSaveToggleKey, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
|
||||
@@ -4432,22 +4432,22 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
</data>
|
||||
<data name="NewPlus.ModuleTitle" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus.ModuleDescription" xml:space="preserve">
|
||||
<value>Create files and folders from a personalized set of templates</value>
|
||||
</data>
|
||||
<data name="NewPlus_Product_Name.Content" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus_Learn_More.Text" xml:space="preserve">
|
||||
<value>Learn more about New+</value>
|
||||
<comment>New+ learn more link. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ learn more link. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus_Enable_Toggle.Header" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>Localize product name in accordance with Windows New</comment>
|
||||
<comment>Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus_TemplatesNotBackupAndRestoreWarning.Title" xml:space="preserve">
|
||||
<value>PowerToys "Back up and Restore" feature doesn't take templates into account at this moment. If you use that feature, templates will have to be copied manually.</value>
|
||||
@@ -4534,7 +4534,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
</data>
|
||||
<data name="Oobe_NewPlus.Title" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="Oobe_NewPlus.Description" xml:space="preserve">
|
||||
<value>Create files and folders from a personalized set of templates.</value>
|
||||
@@ -4942,11 +4942,11 @@ The break timer font matches the text font.</value>
|
||||
<data name="ZoomIt_Snip_Shortcut.Header" xml:space="preserve">
|
||||
<value>Snip activation</value>
|
||||
</data>
|
||||
<data name="ZoomIt_Snip_Shortcut_Save" xml:space="preserve">
|
||||
<value>Press **{0}** to save the snip to a file</value>
|
||||
<data name="ZoomIt_SnipSave_Shortcut.Header" xml:space="preserve">
|
||||
<value>Save snip to file</value>
|
||||
</data>
|
||||
<data name="ZoomIt_Panorama_Shortcut_Save" xml:space="preserve">
|
||||
<value>Press **{0}** to save the scrolling screenshot to a file</value>
|
||||
<data name="ZoomIt_PanoramaSave_Shortcut.Header" xml:space="preserve">
|
||||
<value>Save scrolling screenshot to file</value>
|
||||
</data>
|
||||
<data name="ZoomIt_SnipOcr_Shortcut.Header" xml:space="preserve">
|
||||
<value>Text recognition and extraction</value>
|
||||
|
||||
@@ -416,28 +416,22 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
_zoomItSettings.Properties.SnipToggleKey.Value = value ?? ZoomItProperties.DefaultSnipToggleKey;
|
||||
OnPropertyChanged(nameof(SnipToggleKey));
|
||||
OnPropertyChanged(nameof(SnipToggleKeySave));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HotkeySettings SnipToggleKeySave
|
||||
public HotkeySettings SnipSaveToggleKey
|
||||
{
|
||||
get
|
||||
get => _zoomItSettings.Properties.SnipSaveToggleKey.Value;
|
||||
set
|
||||
{
|
||||
var baseKey = _zoomItSettings.Properties.SnipToggleKey.Value;
|
||||
if (baseKey == null)
|
||||
if (_zoomItSettings.Properties.SnipSaveToggleKey.Value != value)
|
||||
{
|
||||
return null;
|
||||
_zoomItSettings.Properties.SnipSaveToggleKey.Value = value ?? ZoomItProperties.DefaultSnipSaveToggleKey;
|
||||
OnPropertyChanged(nameof(SnipSaveToggleKey));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
return new HotkeySettings(
|
||||
baseKey.Win,
|
||||
baseKey.Ctrl,
|
||||
baseKey.Alt,
|
||||
!baseKey.Shift, // Toggle Shift: if Shift is present, remove it; if absent, add it
|
||||
baseKey.Code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,28 +458,22 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
_zoomItSettings.Properties.SnipPanoramaToggleKey.Value = value ?? ZoomItProperties.DefaultSnipPanoramaToggleKey;
|
||||
OnPropertyChanged(nameof(SnipPanoramaToggleKey));
|
||||
OnPropertyChanged(nameof(SnipPanoramaToggleKeySave));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HotkeySettings SnipPanoramaToggleKeySave
|
||||
public HotkeySettings SnipPanoramaSaveToggleKey
|
||||
{
|
||||
get
|
||||
get => _zoomItSettings.Properties.SnipPanoramaSaveToggleKey.Value;
|
||||
set
|
||||
{
|
||||
var baseKey = _zoomItSettings.Properties.SnipPanoramaToggleKey.Value;
|
||||
if (baseKey == null)
|
||||
if (_zoomItSettings.Properties.SnipPanoramaSaveToggleKey.Value != value)
|
||||
{
|
||||
return null;
|
||||
_zoomItSettings.Properties.SnipPanoramaSaveToggleKey.Value = value ?? ZoomItProperties.DefaultSnipPanoramaSaveToggleKey;
|
||||
OnPropertyChanged(nameof(SnipPanoramaSaveToggleKey));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
return new HotkeySettings(
|
||||
baseKey.Win,
|
||||
baseKey.Ctrl,
|
||||
baseKey.Alt,
|
||||
!baseKey.Shift, // Toggle Shift: if Shift is present, remove it; if absent, add it
|
||||
baseKey.Code);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user