mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-04 09:30:04 +02:00
Compare commits
14 Commits
dev/crutka
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4ef90d168 | ||
|
|
e0fe3c48cf | ||
|
|
029dd04ce4 | ||
|
|
3d3cef73da | ||
|
|
03e5f3e837 | ||
|
|
9039451e2f | ||
|
|
af45c3ec7c | ||
|
|
de4859454c | ||
|
|
93669df118 | ||
|
|
56fabda79c | ||
|
|
70555459ab | ||
|
|
d6319516d0 | ||
|
|
53737cbe31 | ||
|
|
bf6ff579d3 |
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -838,6 +838,7 @@ INTRESOURCE
|
||||
INVALIDARG
|
||||
invalidoperatioexception
|
||||
invokecommand
|
||||
iOS
|
||||
ipcmanager
|
||||
ipreviewhandlervisualssetfont
|
||||
IPTC
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -211,12 +211,12 @@
|
||||
"WinUI3Apps\\NewPlusPackage.msix",
|
||||
"WinUI3Apps\\PowerToys.NewPlus.ShellExtension.win10.dll",
|
||||
|
||||
"PowerAccent.Core.dll",
|
||||
"PowerAccent.Common.dll",
|
||||
"PowerToys.PowerAccent.dll",
|
||||
"PowerToys.PowerAccent.exe",
|
||||
"WinUI3Apps\\PowerAccent.Core.dll",
|
||||
"WinUI3Apps\\PowerAccent.Common.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerAccent.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerAccent.exe",
|
||||
"PowerToys.PowerAccentModuleInterface.dll",
|
||||
"PowerToys.PowerAccentKeyboardService.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerAccentKeyboardService.dll",
|
||||
|
||||
"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
|
||||
|
||||
@@ -806,6 +806,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Core.UnitTests/PowerAccent.Core.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Common/PowerAccent.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -15,14 +15,15 @@ Quick Accent (formerly known as Power Accent) is a PowerToys module that allows
|
||||
|
||||
## Architecture
|
||||
|
||||
The Quick Accent module consists of four main components:
|
||||
The Quick Accent module consists of five projects:
|
||||
|
||||
```
|
||||
poweraccent/
|
||||
├── PowerAccent.Core/ # Core component containing Language Sets
|
||||
├── PowerAccent.UI/ # The character selector UI
|
||||
├── PowerAccentKeyboardService/ # Keyboard Hook
|
||||
└── PowerAccentModuleInterface/ # DLL interface
|
||||
├── PowerAccent.Common/ # Language data, character mappings, LetterKey enum
|
||||
├── PowerAccent.Core/ # Accent logic, settings, positioning, usage statistics
|
||||
├── PowerAccent.UI/ # WinUI 3 character selector app (PowerToys.PowerAccent.exe)
|
||||
├── PowerAccentKeyboardService/ # WinRT keyboard-hook component
|
||||
└── PowerAccentModuleInterface/ # Native runner module DLL
|
||||
```
|
||||
|
||||
### Module Interface (PowerAccentModuleInterface)
|
||||
@@ -32,21 +33,32 @@ The Module Interface, implemented in `PowerAccentModuleInterface/dllmain.cpp`, i
|
||||
- Managing module lifecycle (enable/disable/settings)
|
||||
- Launching and terminating the PowerToys.PowerAccent.exe process
|
||||
|
||||
### Shared Data (PowerAccent.Common)
|
||||
|
||||
`PowerAccent.Common` holds the UI- and runtime-agnostic data the other projects share:
|
||||
- The language / character-set definitions and per-letter accent mappings
|
||||
- The managed `LetterKey` enum (kept in sync with the WinRT `LetterKey` in `PowerAccentKeyboardService/KeyboardListener.idl`)
|
||||
|
||||
It has no UI or WinRT dependencies and is unit-tested in isolation (`PowerAccent.Common.UnitTests`).
|
||||
|
||||
### Core Logic (PowerAccent.Core)
|
||||
|
||||
The Core component contains:
|
||||
- Main accent character logic
|
||||
- Keyboard input detection
|
||||
- Character mappings for different languages
|
||||
- Management of language sets and special characters (currency, math symbols, etc.)
|
||||
- Usage statistics for frequently used characters
|
||||
- Main accent character logic, consuming the language data from `PowerAccent.Common`
|
||||
- Toolbar positioning math (9 anchor points with per-monitor DPI) and settings handling
|
||||
- Management of special characters (currency, math symbols, etc.) and usage statistics
|
||||
|
||||
Core carries no UI-framework dependency: it raises events and accepts a UI-thread marshaller delegate instead of touching WPF/WinUI directly, and its positioning math is covered by `PowerAccent.Core.UnitTests`.
|
||||
|
||||
### UI Layer (PowerAccent.UI)
|
||||
|
||||
The UI component is responsible for:
|
||||
- Displaying the toolbar with accent options
|
||||
- Handling user selection of accented characters
|
||||
- Managing the visual positioning of the toolbar
|
||||
The UI component is a self-contained **WinUI 3 (Windows App SDK)** app, migrated from WPF.
|
||||
It is responsible for:
|
||||
- Displaying the accent toolbar — a non-activating, always-on-top `TransparentWindow` overlay shown with `SW_SHOWNA` so it never steals focus from the app being typed into
|
||||
- Handling selection and the toolbar's sizing / positioning
|
||||
- Following the system theme while the long-lived process runs
|
||||
|
||||
It builds to `PowerToys.PowerAccent.exe` together with its `.pri` and the bundled Windows App SDK runtime, all under the `WinUI3Apps` output folder.
|
||||
|
||||
### Keyboard Service (PowerAccentKeyboardService)
|
||||
|
||||
@@ -59,13 +71,26 @@ This component:
|
||||
|
||||
### Activation Mechanism
|
||||
|
||||
The Quick Accent is activated when:
|
||||
Quick Accent supports two activation styles, selected by the **Activation key** setting.
|
||||
|
||||
**Trigger-key modes** (`Left/Right arrow`, `Space`, or `Both` — the default):
|
||||
1. A user presses and holds a character key (e.g., 'a')
|
||||
2. User presses the trigger key
|
||||
3. After a brief delay (around 300ms per setting), the accent toolbar appears
|
||||
4. The user can select an accented variant using the trigger key
|
||||
5. Upon releasing the keys, the selected accented character is inserted
|
||||
|
||||
**Press-and-hold mode** (`Press and hold the letter`, iOS/macOS style, opt-in):
|
||||
1. A user presses and holds an accent-capable character key (e.g., 'a'); the base
|
||||
letter is typed immediately
|
||||
2. After the configured **Hold duration** (around 500ms per setting), the accent
|
||||
toolbar appears automatically — no separate trigger key is required
|
||||
3. The user navigates the options with the arrow keys or Space
|
||||
4. Upon releasing the letter, the selected accent replaces the base letter; if no
|
||||
option was selected, the base letter that was already typed simply remains
|
||||
5. A quick tap (shorter than the Hold duration) types the base letter only, and
|
||||
modifier combinations (Ctrl/Alt/AltGr/Win + letter) are left untouched
|
||||
|
||||
### Character Sets
|
||||
|
||||
The module includes multiple language-specific character sets and special character sets:
|
||||
@@ -115,5 +140,5 @@ To directly debug the Quick Accent UI component:
|
||||
5. Start debugging by pressing `F5` or clicking the "*Start*" button
|
||||
6. Verify that the debugger breaks at your breakpoint and you can inspect variables and step through code
|
||||
|
||||
**Known issue**: You may encounter approximately 78 errors during the start of debugging.<br>
|
||||
**Solution**: If you encounter errors, right-click on the **PowerAccent** folder in Solution Explorer and select "*Rebuild*". After rebuilding, start debugging again.
|
||||
**Known issue**: A first incremental build can surface transient errors (for example from CsWinRT projection / WinUI XAML codegen ordering).<br>
|
||||
**Solution**: Right-click the **PowerAccent** folder in Solution Explorer and select "*Rebuild*", then start debugging again.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# [CleanUp_tool](/tools/CleanUp_tool/) and [CleanUp_tool_powershell_script](/tools/CleanUp_tool_powershell_script/CleanUp_tool.ps1)
|
||||
|
||||
This tool, respective this powershell script, is used to clean up the PowerToys installation. It cleans the `AppData` folder and the registry.
|
||||
|
||||
This tool is currently very outdated and just cleans up the registry keys of some few modules.
|
||||
@@ -10,7 +10,6 @@ Following tools are currently available:
|
||||
|
||||
* [BugReportTool](bug-report-tool.md) - A tool to collect logs and system information for bug reports.
|
||||
* [Build tools](build-tools.md) - A set of scripts that help building PowerToys.
|
||||
* [Clean up tool](clean-up-tool.md) - A tool to clean up the PowerToys installation.
|
||||
* [Monitor info report](monitor-info-report.md) - A small diagnostic tool which helps identifying WinAPI bugs related to the physical monitor detection.
|
||||
* [project template](/tools/project_template/README.md) - A Visual Studio project template for a new PowerToys project.
|
||||
* [StylesReportTool](styles-report-tool.md) - A tool to collect information about an open window.
|
||||
|
||||
@@ -109,7 +109,7 @@ Per Application/Package one or more Keyboard manifests can be declared. Every ma
|
||||
<details>
|
||||
<summary><b>SectionName</b> - Name of the category of shortcuts</summary>
|
||||
|
||||
Name of the section of shortcuts.
|
||||
Name of the section of shortcuts. Use sentence case, the same convention described under `Name` below.
|
||||
|
||||
**Special sections**:
|
||||
|
||||
@@ -126,6 +126,10 @@ Special sections start with an identifier enclosed between `<` and `>`. This dec
|
||||
|
||||
Name of the shortcut. This is the name that will be displayed in the interpreter.
|
||||
|
||||
**Casing**:
|
||||
|
||||
By convention, shortcut names (and `SectionName` values) use **sentence case**: capitalize only the first word plus any proper nouns or product/feature names. For example, prefer `Reopen last closed tab` over `Reopen Last Closed Tab`, but keep `Open History`, `Quit Slack`, and `Show Quick Access` capitalized because those are application feature names. Match the casing the application uses for its own features rather than copying the title-case styling some apps apply to their entire shortcut list.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.Foundation;
|
||||
using WinUIEx;
|
||||
|
||||
@@ -37,6 +39,18 @@ namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> is delayed until the
|
||||
/// content has finished animating out. With no listener the window simply shows
|
||||
/// or hides immediately.</para>
|
||||
/// <para><b>Multiple surfaces.</b> More than one <see cref="TransientSurface"/>
|
||||
/// may host on the same window by each calling
|
||||
/// <see cref="TransientSurface.SubscribeTo"/>. The <see cref="Showing"/> and
|
||||
/// <see cref="Hiding"/> events are simply raised for every subscriber, and
|
||||
/// because <see cref="HidingEventArgs"/> aggregates deferrals the underlying
|
||||
/// window is hidden only after <em>all</em> surfaces have finished animating
|
||||
/// out. To let each surface play its own distinct transition, call the
|
||||
/// parameterless <see cref="Show()"/> (so every surface uses its configured
|
||||
/// <c>ShowTransition</c>/<c>HideTransition</c>); the <see cref="Show(Transition)"/>
|
||||
/// overload instead broadcasts a single transition to all surfaces. Sizing the
|
||||
/// window and positioning each surface within it remain the consumer's
|
||||
/// responsibility (this window owns no layout).</para>
|
||||
/// </remarks>
|
||||
public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
@@ -52,6 +66,9 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
|
||||
private readonly nint _hwnd;
|
||||
|
||||
private bool _inputHooked;
|
||||
private bool _seenActivated;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
AppWindow.Hide();
|
||||
@@ -74,8 +91,30 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
ApplyExStyleBit(WsExToolWindow, true);
|
||||
|
||||
SystemBackdrop = new TransparentTintBackdrop();
|
||||
|
||||
Activated += OnActivatedForDismiss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether pressing <c>Esc</c> while the
|
||||
/// window content has keyboard focus dismisses the window (<see cref="Hide"/>).
|
||||
/// Defaults to <see langword="false"/>. The window is shown without
|
||||
/// activation, so the consumer must activate it for its content to receive
|
||||
/// keyboard input.
|
||||
/// </summary>
|
||||
public bool DismissOnEscape { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window dismisses itself
|
||||
/// (<see cref="Hide"/>) when it loses focus (is deactivated), i.e. light
|
||||
/// dismiss. Defaults to <see langword="false"/>. Only takes effect after the
|
||||
/// window has been activated at least once since the last <see cref="Show()"/>,
|
||||
/// so the transient deactivation that can occur during the show sequence does
|
||||
/// not dismiss it prematurely. The window is shown without activation, so the
|
||||
/// consumer must activate it for this to apply.
|
||||
/// </summary>
|
||||
public bool DismissOnFocusLost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised (without activation) when <see cref="Show()"/> makes the window
|
||||
/// visible. A content surface subscribes to this to play its in-animation,
|
||||
@@ -112,6 +151,8 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_seenActivated = false;
|
||||
EnsureInputHooks();
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
Showing?.Invoke(this, new ShowingEventArgs(transition));
|
||||
});
|
||||
@@ -134,6 +175,41 @@ public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
});
|
||||
}
|
||||
|
||||
private void OnActivatedForDismiss(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
if (DismissOnFocusLost && _seenActivated)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_seenActivated = true;
|
||||
}
|
||||
|
||||
private void EnsureInputHooks()
|
||||
{
|
||||
if (_inputHooked || Content is not UIElement element)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
element.KeyDown += OnContentKeyDown;
|
||||
_inputHooked = true;
|
||||
}
|
||||
|
||||
private void OnContentKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (DismissOnEscape && e.Key == global::Windows.System.VirtualKey.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyExStyleBit(int bit, bool set)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
|
||||
@@ -32,6 +32,17 @@ namespace EnvironmentVariables
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||
|
||||
// The WinUI TitleBar control reads the owning window's title (AppWindow.Title) during a
|
||||
// deferred layout pass. If the native window title is empty at that instant, the windowing
|
||||
// layer can fault while resolving it and terminate the process. ResourceLoader.GetString
|
||||
// returns an empty string when the resource map can't be resolved at runtime, which would
|
||||
// leave the title empty here, so fall back to a non-empty product name to keep the native
|
||||
// window title populated.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "Environment Variables";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
titleBar.Title = title;
|
||||
|
||||
|
||||
@@ -25,6 +25,15 @@ namespace FileLocksmithUI
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "File Locksmith";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
titleBar.Title = title;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ namespace Hosts
|
||||
var loader = new ResourceLoader("PowerToys.HostsUILib.pri", "PowerToys.HostsUILib/Resources");
|
||||
|
||||
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "Hosts File Editor";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
titleBar.Title = title;
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
PackageName: AgileBits.1Password
|
||||
Name: 1Password
|
||||
WindowFilter: "1Password.exe"
|
||||
BackgroundProcess: false
|
||||
Shortcuts:
|
||||
- SectionName: Basics
|
||||
Properties:
|
||||
- Name: View keyboard shortcuts
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- "/"
|
||||
- Name: Show Quick Access
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Space>"
|
||||
- Name: Lock 1Password
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- L
|
||||
- SectionName: Navigation
|
||||
Properties:
|
||||
- Name: Find
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F
|
||||
- Name: Switch to all accounts
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<1>"
|
||||
- Name: "Switch accounts & collections"
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- '2 - 9'
|
||||
- Name: Back
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- "<Left>"
|
||||
- Name: Forward
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- "<Right>"
|
||||
- Name: Focus next row
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Down>"
|
||||
- Name: Focus previous row
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Up>"
|
||||
- Name: Focus right section
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Right>"
|
||||
- Name: Focus left section
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Left>"
|
||||
- SectionName: Selected item
|
||||
Properties:
|
||||
- Name: Copy primary field
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- C
|
||||
- Name: Copy password
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- C
|
||||
- Name: Copy one-time password
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- C
|
||||
- Name: "Open & fill in web browser"
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- F
|
||||
- Name: Open item in new window
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- O
|
||||
- Name: Edit item
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Save item
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Reveal concealed fields
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- Name: Archive item
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Delete>"
|
||||
- Name: Delete item
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<Delete>"
|
||||
- SectionName: View
|
||||
Properties:
|
||||
- Name: Show/hide sidebar
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Zoom in
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "+"
|
||||
- Name: Zoom out
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "-"
|
||||
- Name: Actual size
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "<0>"
|
||||
@@ -0,0 +1,126 @@
|
||||
PackageName: Zoom.Zoom
|
||||
Name: Zoom Workspace
|
||||
WindowFilter: "zoom.exe"
|
||||
BackgroundProcess: false
|
||||
Shortcuts:
|
||||
- SectionName: General
|
||||
Properties:
|
||||
- Name: Navigate between Zoom windows
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F6
|
||||
- Name: Show or hide floating meeting controls
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: true
|
||||
Keys:
|
||||
- H
|
||||
- SectionName: View
|
||||
Properties:
|
||||
- Name: Switch to active speaker view
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- F1
|
||||
- Name: Switch to gallery view
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- F2
|
||||
- Name: Enter or exit full screen
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- F
|
||||
- Name: View previous page in gallery
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- PageUp
|
||||
- Name: View next page in gallery
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- PageDown
|
||||
- SectionName: Meeting controls
|
||||
Properties:
|
||||
- Name: Mute or unmute audio
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- A
|
||||
- Name: Start or stop video
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- V
|
||||
- Name: Raise or lower hand
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- Y
|
||||
- Name: Open invite window
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- I
|
||||
- Name: Open share screen window or stop sharing
|
||||
Recommended: true
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- S
|
||||
- Name: Pause or resume screen sharing
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- T
|
||||
- Name: Prompt to leave or end meeting
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- Q
|
||||
@@ -57,7 +57,17 @@ namespace ShortcutGuide
|
||||
return _currentApplicationIds;
|
||||
});
|
||||
|
||||
Title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
|
||||
var title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "Shortcut Guide";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
#if !DEBUG
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -160,7 +160,7 @@ bool WindowBorder::Init(HINSTANCE hinstance)
|
||||
|
||||
void WindowBorder::UpdateBorderPosition() const
|
||||
{
|
||||
if (!m_trackingWindow)
|
||||
if (!m_trackingWindow || !m_frameDrawer || !m_window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,16 @@ public sealed partial class CmdPalMainControl : UserControl
|
||||
return CardBorder.ActualHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <paramref name="stretch"/> is <see langword="true"/>, the card stretches to fill
|
||||
/// the entire window vertically (non-compact mode). When <see langword="false"/>, the card
|
||||
/// sizes itself to its content and anchors to the top of the window (compact mode).
|
||||
/// </summary>
|
||||
public void SetCardStretch(bool stretch)
|
||||
{
|
||||
CardBorder.VerticalAlignment = stretch ? VerticalAlignment.Stretch : VerticalAlignment.Top;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards the host window's activation state to the current backdrop so the system can
|
||||
/// render its active / inactive appearance correctly.
|
||||
|
||||
@@ -1759,17 +1759,26 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
|
||||
// Only the compact + centered configuration needs a screen-fit clamp. There the card
|
||||
// is anchored near the vertical center of the display, so an expanded list could run
|
||||
// off the bottom edge; cap its height so it always fits. In every other case the card
|
||||
// is free to fill the (fixed-size) HWND as before.
|
||||
if (expanded && settings.CompactMode && IsCenteringSummon(settings))
|
||||
if (!settings.CompactMode)
|
||||
{
|
||||
RootElement.SetCardMaxHeight(ComputeExpandedCardMaxHeightDip());
|
||||
// When compact mode is off the card is always static and fills the entire window,
|
||||
// regardless of how much content is currently displayed.
|
||||
RootElement.SetCardStretch(true);
|
||||
RootElement.SetCardMaxHeight(double.PositiveInfinity);
|
||||
}
|
||||
else
|
||||
{
|
||||
RootElement.SetCardMaxHeight(double.PositiveInfinity);
|
||||
// In compact mode the card sizes itself to its content and anchors to the top.
|
||||
RootElement.SetCardStretch(false);
|
||||
|
||||
// Only the compact + centered configuration needs a screen-fit clamp. There the card
|
||||
// is anchored near the vertical center of the display, so an expanded list could run
|
||||
// off the bottom edge; cap its height so it always fits. In every other case the card
|
||||
// is free to fill the (fixed-size) HWND as before.
|
||||
var cardMaxHeight = expanded && IsCenteringSummon(settings)
|
||||
? ComputeExpandedCardMaxHeightDip()
|
||||
: double.PositiveInfinity;
|
||||
RootElement.SetCardMaxHeight(cardMaxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private readonly CompositeFormat _pageNavigatedAnnouncement;
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
// The last compact-mode setting we reacted to. Lets us ignore hot-reloads of unrelated
|
||||
// settings and only re-evaluate the layout when compact mode itself changes.
|
||||
private bool _compactMode;
|
||||
|
||||
private SettingsWindow? _settingsWindow;
|
||||
private DockWindowManager? _dockWindowManager;
|
||||
|
||||
@@ -91,8 +97,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public ShellPage()
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
this.ExpandedMode = !settings.CompactMode;
|
||||
_settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
|
||||
_compactMode = _settingsService.Settings.CompactMode;
|
||||
this.ExpandedMode = !_compactMode;
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
@@ -119,6 +126,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ExpandCompactModeMessage>(this);
|
||||
|
||||
// The compact-mode setting can be toggled while the palette is open. React to the
|
||||
// hot-reload so the expanded/collapsed layout updates immediately instead of waiting
|
||||
// for the next navigation or search-text change.
|
||||
_settingsService.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
@@ -674,6 +686,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
if (!settings.CompactMode)
|
||||
{
|
||||
// Compact mode is off: the shell always shows the full expanded UI. Set it
|
||||
// explicitly (rather than trusting the constructor's initial value) so toggling
|
||||
// the setting off at runtime restores the list and command bar when the palette
|
||||
// was collapsed.
|
||||
HandleExpandCompactOnUiThread(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -936,6 +953,24 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
this.DispatcherQueue.TryEnqueue(UpdateCompactModeForCurrentPage);
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(ISettingsService sender, SettingsModel args)
|
||||
{
|
||||
// Only the compact-mode setting affects the expanded/collapsed layout, so ignore
|
||||
// hot-reloads that leave it unchanged. Comparing and updating _compactMode on the UI
|
||||
// thread keeps it single-threaded regardless of which thread raises the event.
|
||||
var compactMode = args.CompactMode;
|
||||
this.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (compactMode == _compactMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_compactMode = compactMode;
|
||||
UpdateCompactModeForCurrentPage();
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleExpandCompactOnUiThread(bool expanded)
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
@@ -979,6 +1014,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
_isDisposed = true;
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
_settingsService.SettingsChanged -= OnSettingsChanged;
|
||||
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Peek.UI
|
||||
public MainWindowViewModel ViewModel { get; }
|
||||
|
||||
private readonly ThemeListener? themeListener;
|
||||
private readonly IUserSettings userSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the delete confirmation dialog is currently open. Used to ensure only one
|
||||
@@ -66,6 +67,19 @@ namespace Peek.UI
|
||||
AppWindow.SetIcon("Assets/Peek/Icon.ico");
|
||||
|
||||
AppWindow.Closing += AppWindow_Closing;
|
||||
|
||||
userSettings = Application.Current.GetService<IUserSettings>();
|
||||
userSettings.Changed += UpdateWindowBySettings;
|
||||
UpdateWindowBySettings(null, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void UpdateWindowBySettings(object? sender, EventArgs e)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
IsAlwaysOnTop = userSettings.AlwaysOnTop;
|
||||
IsShownInSwitchers = userSettings.ShowTaskbarIcon;
|
||||
});
|
||||
}
|
||||
|
||||
private async void Content_KeyUp(object sender, KeyRoutedEventArgs e)
|
||||
@@ -88,7 +102,7 @@ namespace Peek.UI
|
||||
{
|
||||
_isDeleteInProgress = true;
|
||||
|
||||
if (Application.Current.GetService<IUserSettings>().ConfirmFileDelete)
|
||||
if (userSettings.ConfirmFileDelete)
|
||||
{
|
||||
if (await ShowDeleteConfirmationDialogAsync() == ContentDialogResult.Primary)
|
||||
{
|
||||
@@ -341,6 +355,7 @@ namespace Peek.UI
|
||||
public void Dispose()
|
||||
{
|
||||
themeListener?.Dispose();
|
||||
userSettings.Changed -= UpdateWindowBySettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
// 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;
|
||||
|
||||
namespace Peek.UI
|
||||
{
|
||||
public interface IUserSettings
|
||||
{
|
||||
public bool AlwaysOnTop { get; }
|
||||
|
||||
public bool ShowTaskbarIcon { get; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public bool ConfirmFileDelete { get; set; }
|
||||
|
||||
public bool ShowFilePreviewTooltip { get; }
|
||||
|
||||
public event EventHandler? Changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,13 +36,29 @@ namespace Peek.UI
|
||||
lock (_settingsLock)
|
||||
{
|
||||
_settings = value;
|
||||
AlwaysOnTop = _settings.Properties.AlwaysOnTop.Value;
|
||||
ShowTaskbarIcon = _settings.Properties.ShowTaskbarIcon.Value;
|
||||
CloseAfterLosingFocus = _settings.Properties.CloseAfterLosingFocus.Value;
|
||||
ConfirmFileDelete = _settings.Properties.ConfirmFileDelete.Value;
|
||||
ShowFilePreviewTooltip = _settings.Properties.ShowFilePreviewTooltip.Value;
|
||||
}
|
||||
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? Changed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Peek shows its window on the top of the stack.
|
||||
/// </summary>
|
||||
public bool AlwaysOnTop { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Peek shows its icon on the taskbar when activated.
|
||||
/// </summary>
|
||||
public bool ShowTaskbarIcon { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Peek closes automatically when the window loses focus.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Currently hard-coded, as this project does not target WinRT.
|
||||
@@ -8,6 +9,10 @@
|
||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<!-- Required by the CsWinRT AOT optimizer: marshaling generic collections (e.g. the
|
||||
Dictionary<Language, LanguageInfo> in CharacterMappings) across the WinRT ABI emits
|
||||
unsafe code. Matches the sibling AOT-compatible library PowerDisplay.Lib. -->
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerAccent.Core;
|
||||
using PowerAccent.Core.Services;
|
||||
using PowerAccent.Core.Tools;
|
||||
|
||||
namespace PowerAccent.Core.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Exercises the pure anchor / DPI geometry in <see cref="Calculation"/>. These are the math that
|
||||
/// the WinUI 3 Selector feeds into AppWindow.Move/Resize, so a regression here silently mis-places
|
||||
/// the accent popup (the classic high-DPI / multi-monitor "double scaling" failure mode).
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class CalculationTests
|
||||
{
|
||||
// offset baked into Calculation: the gap from the screen edge for the edge anchors.
|
||||
private const int Offset = 24;
|
||||
|
||||
// A 1920x1080 primary monitor rooted at the virtual-desktop origin.
|
||||
private static readonly Rect PrimaryScreen = new(0, 0, 1920, 1080);
|
||||
|
||||
// A one-row accent bar, in DIP.
|
||||
private static readonly Size Window = new(200, 52);
|
||||
|
||||
// At 100% scaling (dpi = 1.0) the physical window size equals the DIP size, so each of the nine
|
||||
// anchors lands at an easily hand-checkable coordinate.
|
||||
[DataTestMethod]
|
||||
[DataRow(Position.TopLeft, 24.0, 24.0)]
|
||||
[DataRow(Position.Top, 860.0, 24.0)]
|
||||
[DataRow(Position.TopRight, 1696.0, 24.0)]
|
||||
[DataRow(Position.Left, 24.0, 514.0)]
|
||||
[DataRow(Position.Center, 860.0, 514.0)]
|
||||
[DataRow(Position.Right, 1696.0, 514.0)]
|
||||
[DataRow(Position.BottomLeft, 24.0, 1004.0)]
|
||||
[DataRow(Position.Bottom, 860.0, 1004.0)]
|
||||
[DataRow(Position.BottomRight, 1696.0, 1004.0)]
|
||||
public void GetRawCoordinatesFromPosition_AtDpi1_PlacesEachAnchor(Position position, double expectedX, double expectedY)
|
||||
{
|
||||
var point = Calculation.GetRawCoordinatesFromPosition(position, PrimaryScreen, Window, dpi: 1.0);
|
||||
|
||||
Assert.AreEqual(expectedX, point.X, "X for " + position);
|
||||
Assert.AreEqual(expectedY, point.Y, "Y for " + position);
|
||||
}
|
||||
|
||||
// At 150% scaling the physical window is 300x78. The centered anchors must subtract HALF of the
|
||||
// scaled size (not the DIP size) and the right/bottom anchors must subtract the FULL scaled size
|
||||
// plus the offset - this is exactly where a missing/extra dpi factor shows up.
|
||||
[DataTestMethod]
|
||||
[DataRow(Position.TopLeft, 24.0, 24.0)]
|
||||
[DataRow(Position.Center, 810.0, 501.0)]
|
||||
[DataRow(Position.BottomRight, 1596.0, 978.0)]
|
||||
public void GetRawCoordinatesFromPosition_AtDpi150Percent_ScalesWindowFootprint(Position position, double expectedX, double expectedY)
|
||||
{
|
||||
var point = Calculation.GetRawCoordinatesFromPosition(position, PrimaryScreen, Window, dpi: 1.5);
|
||||
|
||||
Assert.AreEqual(expectedX, point.X, "X for " + position);
|
||||
Assert.AreEqual(expectedY, point.Y, "Y for " + position);
|
||||
}
|
||||
|
||||
// A secondary 2560x1440 monitor to the right of the primary at 200% scaling. Verifies the screen
|
||||
// origin (screen.X / screen.Y) is honored for every anchor, not just the primary-at-origin case.
|
||||
[DataTestMethod]
|
||||
[DataRow(Position.TopLeft, 1944.0, 24.0)]
|
||||
[DataRow(Position.Center, 3000.0, 668.0)]
|
||||
[DataRow(Position.BottomRight, 4056.0, 1312.0)]
|
||||
public void GetRawCoordinatesFromPosition_OnOffsetMonitor_HonorsScreenOrigin(Position position, double expectedX, double expectedY)
|
||||
{
|
||||
var secondaryScreen = new Rect(1920, 0, 2560, 1440);
|
||||
|
||||
var point = Calculation.GetRawCoordinatesFromPosition(position, secondaryScreen, Window, dpi: 2.0);
|
||||
|
||||
Assert.AreEqual(expectedX, point.X, "X for " + position);
|
||||
Assert.AreEqual(expectedY, point.Y, "Y for " + position);
|
||||
}
|
||||
|
||||
// A monitor positioned to the LEFT of the primary has a negative virtual-desktop X origin. The
|
||||
// edge anchors must still be offset relative to that negative origin.
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_OnNegativeOriginMonitor_OffsetsFromScreenEdge()
|
||||
{
|
||||
var leftScreen = new Rect(-1920, 0, 1920, 1080);
|
||||
|
||||
var topLeft = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, leftScreen, Window, dpi: 1.0);
|
||||
Assert.AreEqual(-1920 + Offset, topLeft.X);
|
||||
Assert.AreEqual(Offset, topLeft.Y);
|
||||
|
||||
var bottomRight = Calculation.GetRawCoordinatesFromPosition(Position.BottomRight, leftScreen, Window, dpi: 1.0);
|
||||
Assert.AreEqual(-1920 + 1920 - (Window.Width + Offset), bottomRight.X);
|
||||
Assert.AreEqual(1080 - (Window.Height + Offset), bottomRight.Y);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_UnknownPosition_Throws()
|
||||
{
|
||||
Assert.ThrowsException<NotImplementedException>(
|
||||
() => Calculation.GetRawCoordinatesFromPosition((Position)999, PrimaryScreen, Window, dpi: 1.0));
|
||||
}
|
||||
|
||||
// Caret-relative placement centers the window horizontally on the caret and sits it 20px above.
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_WithRoom_CentersAboveCaret()
|
||||
{
|
||||
var caret = new Point(960, 540);
|
||||
|
||||
var point = Calculation.GetRawCoordinatesFromCaret(caret, PrimaryScreen, Window);
|
||||
|
||||
Assert.AreEqual(960 - (Window.Width / 2), point.X); // 860
|
||||
Assert.AreEqual(540 - Window.Height - 20, point.Y); // 468
|
||||
}
|
||||
|
||||
// Near the left edge the window would overflow off-screen, so X clamps to the screen's left edge.
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_NearLeftEdge_ClampsToScreenLeft()
|
||||
{
|
||||
var caret = new Point(50, 540);
|
||||
|
||||
var point = Calculation.GetRawCoordinatesFromCaret(caret, PrimaryScreen, Window);
|
||||
|
||||
Assert.AreEqual(PrimaryScreen.X, point.X);
|
||||
}
|
||||
|
||||
// Near the right edge X clamps so the window's right side sits on the screen's right edge.
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_NearRightEdge_ClampsToScreenRight()
|
||||
{
|
||||
var caret = new Point(1900, 540);
|
||||
|
||||
var point = Calculation.GetRawCoordinatesFromCaret(caret, PrimaryScreen, Window);
|
||||
|
||||
Assert.AreEqual(PrimaryScreen.X + PrimaryScreen.Width - Window.Width, point.X); // 1720
|
||||
}
|
||||
|
||||
// When there is no room above the caret (top would land off-screen) the window flips to 20px
|
||||
// BELOW the caret instead of being clipped at the top.
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_NoRoomAbove_FlipsBelowCaret()
|
||||
{
|
||||
var caret = new Point(960, 10);
|
||||
|
||||
var point = Calculation.GetRawCoordinatesFromCaret(caret, PrimaryScreen, Window);
|
||||
|
||||
Assert.AreEqual(caret.Y + 20, point.Y); // 30
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>PowerToys.PowerAccent.Core.UnitTests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\PowerAccent.Core.UnitTests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
Do NOT set CsWinRTIncludes here. PowerAccent.Core already projects PowerToys.GPOWrapper and
|
||||
PowerToys.PowerAccentKeyboardService, and those managed projections arrive transitively through
|
||||
the PowerAccent.Core project reference. Listing either here generates a SECOND copy and breaks
|
||||
the build with CS0436 (matches PowerAccent.UI, which references Core the same way).
|
||||
-->
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PowerAccent.Core\PowerAccent.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -6,12 +6,6 @@ namespace PowerAccent.Core;
|
||||
|
||||
public struct Point
|
||||
{
|
||||
public Point()
|
||||
{
|
||||
X = 0;
|
||||
Y = 0;
|
||||
}
|
||||
|
||||
public Point(double x, double y)
|
||||
{
|
||||
X = x;
|
||||
@@ -24,35 +18,7 @@ public struct Point
|
||||
Y = y;
|
||||
}
|
||||
|
||||
public Point(System.Drawing.Point point)
|
||||
{
|
||||
X = point.X;
|
||||
Y = point.Y;
|
||||
}
|
||||
|
||||
public double X { get; init; }
|
||||
|
||||
public double Y { get; init; }
|
||||
|
||||
public static implicit operator Point(System.Drawing.Point point) => new Point(point.X, point.Y);
|
||||
|
||||
public static Point operator /(Point point, double divider)
|
||||
{
|
||||
if (divider == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
return new Point(point.X / divider, point.Y / divider);
|
||||
}
|
||||
|
||||
public static Point operator /(Point point, Point divider)
|
||||
{
|
||||
if (divider.X == 0 || divider.Y == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
return new Point(point.X / divider.X, point.Y / divider.Y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@ namespace PowerAccent.Core;
|
||||
|
||||
public struct Rect
|
||||
{
|
||||
public Rect()
|
||||
{
|
||||
X = 0;
|
||||
Y = 0;
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
}
|
||||
|
||||
public Rect(int x, int y, int width, int height)
|
||||
{
|
||||
X = x;
|
||||
@@ -22,14 +14,6 @@ public struct Rect
|
||||
Height = height;
|
||||
}
|
||||
|
||||
public Rect(double x, double y, double width, double height)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
public Rect(Point coord, Size size)
|
||||
{
|
||||
X = coord.X;
|
||||
@@ -45,24 +29,4 @@ public struct Rect
|
||||
public double Width { get; init; }
|
||||
|
||||
public double Height { get; init; }
|
||||
|
||||
public static Rect operator /(Rect rect, double divider)
|
||||
{
|
||||
if (divider == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
return new Rect(rect.X / divider, rect.Y / divider, rect.Width / divider, rect.Height / divider);
|
||||
}
|
||||
|
||||
public static Rect operator /(Rect rect, Rect divider)
|
||||
{
|
||||
if (divider.X == 0 || divider.Y == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
return new Rect(rect.X / divider.X, rect.Y / divider.Y, rect.Width / divider.Width, rect.Height / divider.Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ namespace PowerAccent.Core;
|
||||
|
||||
public struct Size
|
||||
{
|
||||
public Size()
|
||||
{
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
}
|
||||
|
||||
public Size(double width, double height)
|
||||
{
|
||||
Width = width;
|
||||
@@ -27,26 +21,4 @@ public struct Size
|
||||
public double Width { get; init; }
|
||||
|
||||
public double Height { get; init; }
|
||||
|
||||
public static implicit operator Size(System.Drawing.Size size) => new Size(size.Width, size.Height);
|
||||
|
||||
public static Size operator /(Size size, double divider)
|
||||
{
|
||||
if (divider == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
return new Size(size.Width / divider, size.Height / divider);
|
||||
}
|
||||
|
||||
public static Size operator /(Size size, Size divider)
|
||||
{
|
||||
if (divider.Width == 0 || divider.Height == 0 || divider.Width == 0 || divider.Height == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
return new Size(size.Width / divider.Width, size.Height / divider.Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -26,6 +24,13 @@
|
||||
<PackageReference Include="UnicodeInformation" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Expose internal helpers (e.g. Tools.Calculation) to the unit-test assembly. -->
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>PowerToys.PowerAccent.Core.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
|
||||
@@ -11,6 +11,8 @@ using PowerAccent.Core.Services;
|
||||
using PowerAccent.Core.Tools;
|
||||
using PowerToys.PowerAccentKeyboardService;
|
||||
|
||||
using PowerAccentActivationKey = Microsoft.PowerToys.Settings.UI.Library.Enumerations.PowerAccentActivationKey;
|
||||
|
||||
namespace PowerAccent.Core;
|
||||
|
||||
public partial class PowerAccent : IDisposable
|
||||
@@ -43,8 +45,12 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
private readonly CharactersUsageInfo _usageInfo;
|
||||
|
||||
public PowerAccent()
|
||||
private readonly Action<Action> _runOnUiThread;
|
||||
|
||||
public PowerAccent(Action<Action> runOnUiThread)
|
||||
{
|
||||
_runOnUiThread = runOnUiThread ?? throw new ArgumentNullException(nameof(runOnUiThread));
|
||||
|
||||
Logger.InitializeLogger("\\QuickAccent\\Logs");
|
||||
|
||||
LoadUnicodeInfoCache();
|
||||
@@ -66,7 +72,7 @@ public partial class PowerAccent : IDisposable
|
||||
{
|
||||
_keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey) =>
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
_runOnUiThread(() =>
|
||||
{
|
||||
ShowToolbar(letterKey);
|
||||
});
|
||||
@@ -74,7 +80,7 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
_keyboardListener.SetHideToolbarEvent(new PowerToys.PowerAccentKeyboardService.HideToolbar((InputType inputType) =>
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
_runOnUiThread(() =>
|
||||
{
|
||||
SendInputAndHideToolbar(inputType);
|
||||
});
|
||||
@@ -82,7 +88,7 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
_keyboardListener.SetNextCharEvent(new PowerToys.PowerAccentKeyboardService.NextChar((TriggerKey triggerKey, bool shiftPressed) =>
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
_runOnUiThread(() =>
|
||||
{
|
||||
ProcessNextChar(triggerKey, shiftPressed);
|
||||
});
|
||||
@@ -96,28 +102,49 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
private void ShowToolbar(LetterKey letterKey)
|
||||
{
|
||||
_initialShiftState = WindowsFunctions.IsShiftState();
|
||||
_visible = true;
|
||||
|
||||
bool isPressAndHold = _settingService.ActivationKey == PowerAccentActivationKey.PressAndHold;
|
||||
|
||||
// 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;
|
||||
// Trigger modes navigate the instant the toolbar is summoned, so the character data must
|
||||
// be ready synchronously. Press-and-hold can't navigate until the popup is actually shown,
|
||||
// so defer the (relatively expensive) character/description build to the delayed render and
|
||||
// keep quick taps off the keystroke hot path.
|
||||
if (!isPressAndHold)
|
||||
{
|
||||
PrepareCharacters(letterKey);
|
||||
}
|
||||
|
||||
Task.Delay(_settingService.InputTime).ContinueWith(
|
||||
int displayDelay = isPressAndHold ? _settingService.HoldDuration : _settingService.InputTime;
|
||||
|
||||
Task.Delay(displayDelay).ContinueWith(
|
||||
t =>
|
||||
{
|
||||
if (_visible && generation == _showGeneration)
|
||||
{
|
||||
if (isPressAndHold)
|
||||
{
|
||||
PrepareCharacters(letterKey);
|
||||
}
|
||||
|
||||
OnChangeDisplay?.Invoke(true, _characters);
|
||||
}
|
||||
},
|
||||
TaskScheduler.FromCurrentSynchronizationContext());
|
||||
}
|
||||
|
||||
private void PrepareCharacters(LetterKey letterKey)
|
||||
{
|
||||
_initialShiftState = WindowsFunctions.IsShiftState();
|
||||
_characters = GetCharacters(letterKey);
|
||||
_characterDescriptions = GetCharacterDescriptions(_characters);
|
||||
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
|
||||
}
|
||||
|
||||
private string[] GetCharacters(LetterKey letterKey)
|
||||
{
|
||||
var characters = CharacterMappings.GetCharacters(letterKey, _settingService.SelectedLang);
|
||||
@@ -213,13 +240,13 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
case InputType.Right:
|
||||
{
|
||||
SendKeys.SendWait("{RIGHT}");
|
||||
WindowsFunctions.SendArrowKey(left: false);
|
||||
break;
|
||||
}
|
||||
|
||||
case InputType.Left:
|
||||
{
|
||||
SendKeys.SendWait("{LEFT}");
|
||||
WindowsFunctions.SendArrowKey(left: true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -247,6 +274,13 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
|
||||
{
|
||||
// Press-and-hold builds its character set lazily when the popup renders; ignore any
|
||||
// navigation that races ahead of it (there is nothing to select yet).
|
||||
if (_characters.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use an async hardware check as a fallback in case the keyboard hook misses a
|
||||
// quick Shift press. If the popup was opened while holding Shift (e.g., typing a
|
||||
// capital letter), ignore the hardware check so we don't accidentally trigger a
|
||||
@@ -361,14 +395,13 @@ public partial class PowerAccent : IDisposable
|
||||
/// Gets the maximum width for the toolbar display based on the active screen
|
||||
/// dimensions.
|
||||
/// </summary>
|
||||
/// <returns>The maximum width in logical pixels, accounting for screen padding.
|
||||
/// </returns>
|
||||
/// <returns>The maximum width in DIPs (device-independent pixels), accounting for
|
||||
/// screen padding.</returns>
|
||||
public double GetDisplayMaxWidth()
|
||||
{
|
||||
// Note: activeDisplay.Size.Width is in raw physical pixels.
|
||||
// We divide by DPI to convert to WPF logical pixels (Device-Independent Pixels),
|
||||
// because ScreenMinPadding is a logical pixel value and WPF MaxWidth expects
|
||||
// logical pixels.
|
||||
// activeDisplay.Size.Width is in raw physical pixels; divide by the DPI scale to
|
||||
// convert to DIPs (device-independent pixels), since ScreenMinPadding and the
|
||||
// consuming window width are both expressed in DIPs.
|
||||
var activeDisplay = WindowsFunctions.GetActiveDisplay();
|
||||
return (activeDisplay.Size.Width / activeDisplay.Dpi) - ScreenMinPadding;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ public class SettingsService
|
||||
InputTime = settings.Properties.InputTime.Value;
|
||||
_keyboardListener.UpdateInputTime(InputTime);
|
||||
|
||||
HoldDuration = settings.Properties.HoldDuration.Value;
|
||||
_keyboardListener.UpdateHoldDuration(HoldDuration);
|
||||
|
||||
ExcludedApps = settings.Properties.ExcludedApps.Value;
|
||||
_keyboardListener.UpdateExcludedApps(ExcludedApps);
|
||||
|
||||
@@ -196,6 +199,8 @@ public class SettingsService
|
||||
}
|
||||
}
|
||||
|
||||
public int HoldDuration { get; set; } = PowerAccentSettings.DefaultHoldDurationMs;
|
||||
|
||||
private string _excludedApps;
|
||||
|
||||
public string ExcludedApps
|
||||
|
||||
@@ -88,6 +88,40 @@ internal static class WindowsFunctions
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendArrowKey(bool left)
|
||||
{
|
||||
var key = left ? VIRTUAL_KEY.VK_LEFT : VIRTUAL_KEY.VK_RIGHT;
|
||||
var inputs = new INPUT[]
|
||||
{
|
||||
new INPUT
|
||||
{
|
||||
type = INPUT_TYPE.INPUT_KEYBOARD,
|
||||
Anonymous = new INPUT._Anonymous_e__Union
|
||||
{
|
||||
ki = new KEYBDINPUT
|
||||
{
|
||||
wVk = key,
|
||||
dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_EXTENDEDKEY,
|
||||
},
|
||||
},
|
||||
},
|
||||
new INPUT
|
||||
{
|
||||
type = INPUT_TYPE.INPUT_KEYBOARD,
|
||||
Anonymous = new INPUT._Anonymous_e__Union
|
||||
{
|
||||
ki = new KEYBDINPUT
|
||||
{
|
||||
wVk = key,
|
||||
dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_EXTENDEDKEY | KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
_ = PInvoke.SendInput(inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
public static (Point Location, Size Size, double Dpi) GetActiveDisplay()
|
||||
{
|
||||
GUITHREADINFO guiInfo = default;
|
||||
@@ -107,7 +141,8 @@ internal static class WindowsFunctions
|
||||
|
||||
double dpi = dpiRaw / 96d;
|
||||
var location = new Point(monitorInfo.rcWork.left, monitorInfo.rcWork.top);
|
||||
return (location, monitorInfo.rcWork.Size, dpi);
|
||||
var size = new Size(monitorInfo.rcWork.right - monitorInfo.rcWork.left, monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
|
||||
return (location, size, dpi);
|
||||
}
|
||||
|
||||
public static bool IsCapsLockState()
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<Application
|
||||
x:Class="PowerAccent.UI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="Selector.xaml"
|
||||
ThemeMode="System" />
|
||||
@@ -1,64 +0,0 @@
|
||||
// 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;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
|
||||
namespace PowerAccent.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private static Mutex _mutex;
|
||||
private bool _disposed;
|
||||
private ETWTrace _etwTrace = new ETWTrace();
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
_mutex = new Mutex(true, "QuickAccent", out bool createdNew);
|
||||
|
||||
if (!createdNew)
|
||||
{
|
||||
Logger.LogWarning("Another running QuickAccent instance was detected. Exiting QuickAccent");
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
base.OnStartup(e);
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
_mutex?.ReleaseMutex();
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_mutex?.Dispose();
|
||||
_etwTrace?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// 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.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located (used if a resource is not found in the page, or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is locate (used if a resource is not found in the page, app, or any theme specific resource dictionaries)
|
||||
]
|
||||
@@ -1,2 +0,0 @@
|
||||
SetWindowPos
|
||||
GetSystemMetrics
|
||||
@@ -2,39 +2,110 @@
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<Nullable>disable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AssemblyName>PowerToys.PowerAccent</AssemblyName>
|
||||
<XamlDebuggingInformation>True</XamlDebuggingInformation>
|
||||
<StartupObject>PowerAccent.UI.Program</StartupObject>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="icon.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PowerAccent.UI</RootNamespace>
|
||||
<AssemblyName>PowerToys.PowerAccent</AssemblyName>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<!-- Required so CommunityToolkit.Mvvm's source generator emits the WinRT-correct partial
|
||||
property implementations for [ObservableProperty] (avoids MVVMTK0045 / CS9248).
|
||||
Matches the sibling WinUI 3 module PowerDisplay. -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<!--
|
||||
App.xaml and the windows live under PowerAccentXAML\ (not the project root). Nesting the XAML
|
||||
in a named subfolder is the repo convention for WinUI 3 apps that share the WinUI3Apps output
|
||||
folder (see Peek's PeekXAML\, PowerDisplay's PowerDisplayXAML\): it keeps the compiled .xbf out
|
||||
of the WinUI3Apps root, so the "Audit WinAppSDK applications path asset conflicts" pipeline step
|
||||
passes. Disable the default ApplicationDefinition glob so the explicit
|
||||
<ApplicationDefinition Include="PowerAccentXAML\App.xaml" /> below is the single one.
|
||||
-->
|
||||
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<StartupObject>PowerAccent.UI.Program</StartupObject>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<ProjectPriFileName>PowerToys.PowerAccent.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<!-- Native AOT Configuration. Mirrors the sibling WinUI 3 module PowerDisplay so the app is
|
||||
compiled with ILC on publish, surfacing trim/AOT problems that the analyzers alone miss. -->
|
||||
<PropertyGroup>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
|
||||
<PublishAot>true</PublishAot>
|
||||
<TrimmerSingleWarn>false</TrimmerSingleWarn>
|
||||
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\PowerAccent.Core\PowerAccent.Core.csproj" />
|
||||
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
Do NOT set CsWinRTIncludes here. Both WinRT components the UI touches - PowerToys.GPOWrapper
|
||||
(called directly from Program.cs) and PowerToys.PowerAccentKeyboardService (used by Core) -
|
||||
are already projected by PowerAccent.Core, which the UI references, so their managed
|
||||
projections arrive transitively. Listing either here generates a SECOND copy of the same
|
||||
types and breaks the build with CS0436 (e.g. GpoRuleConfigured defined both in this project's
|
||||
generated files and in PowerAccent.Core). This matches the original WPF UI, which had no
|
||||
CsWinRTIncludes at all.
|
||||
-->
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="icon.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyPRIFileToOutputDir" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<PRIFile Include="$(OutDir)**\PowerToys.PowerAccent.pri" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(PRIFile)" DestinationFolder="$(OutDir)" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="PowerAccentXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="PowerAccentXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="WinUIEx" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\PowerAccent.Core\PowerAccent.Core.csproj" />
|
||||
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application
|
||||
x:Class="PowerAccent.UI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace PowerAccent.UI;
|
||||
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private readonly ETWTrace _etwTrace = new ETWTrace();
|
||||
private bool _disposed;
|
||||
|
||||
public static new App Current => (App)Application.Current;
|
||||
|
||||
public DispatcherQueue DispatcherQueueForApp { get; private set; }
|
||||
|
||||
public static MainWindow Window { get; private set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
UnhandledException += (s, e) => Logger.LogError("Unhandled exception", e.Exception);
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
DispatcherQueueForApp = DispatcherQueue.GetForCurrentThread();
|
||||
Window = new MainWindow();
|
||||
|
||||
// Quick Accent has no visible main window until summoned by the keyboard hook;
|
||||
// the accent selector keeps itself hidden (TransparentWindow hides its AppWindow on init).
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_etwTrace?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<common:TransparentWindow
|
||||
x:Class="PowerAccent.UI.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:common="using:Microsoft.PowerToys.Common.UI.Controls.Window"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:PowerAccent.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<!--
|
||||
The content lives in a UserControl (SelectorControl) rather than inline here so its x:Bind
|
||||
bindings initialize on the control's Loading pass - which fires when this SW_SHOWNA overlay is
|
||||
first laid out - instead of on Window.Activated, which never fires for a window shown without
|
||||
activation. That removes the need to call Bindings.Update() by hand.
|
||||
-->
|
||||
<local:SelectorControl x:Name="Selector" />
|
||||
</common:TransparentWindow>
|
||||
@@ -0,0 +1,177 @@
|
||||
// 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;
|
||||
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Windows.Graphics;
|
||||
using CoreSize = PowerAccent.Core.Size;
|
||||
|
||||
namespace PowerAccent.UI;
|
||||
|
||||
public sealed partial class MainWindow : TransparentWindow, IDisposable
|
||||
{
|
||||
// Accent-bar geometry (DIP). Width is derived from the item count (count * ItemWidthDip), not
|
||||
// measured from the ListView: its DesiredSize (wrapped in a ScrollViewer) is racy while item
|
||||
// containers realize and intermittently reports 0, yielding a blank/clipped bar. The one-row bar
|
||||
// hugs its content like the WPF original, capped at the monitor width; beyond that it scrolls
|
||||
// and ScrollIntoView reveals the selected glyph.
|
||||
private const double RowHeightDip = 92; // one row of accent pills (item Height=48 + card border)
|
||||
private const double DescriptionHeightDip = 36; // extra row shown when the Unicode description is on
|
||||
private const double ItemWidthDip = 48; // one accent cell (ListViewItem Grid MinWidth=48)
|
||||
private const double DescriptionMinWidthDip = 648; // min bar width while the description row shows (WPF parity)
|
||||
|
||||
private readonly Core.PowerAccent _powerAccent;
|
||||
private int _selectedIndex = -1;
|
||||
private bool _active;
|
||||
|
||||
// The view model lives on the SelectorControl (the x:Bind target); expose it here for the
|
||||
// PowerAccent event handlers that populate the accent list and description.
|
||||
private SelectorViewModel ViewModel => Selector.ViewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Give the overlay a stable UIA identity (window name) for accessibility tools (Narrator,
|
||||
// Accessibility Insights) and the release-verification harness. "Quick Accent" is the
|
||||
// user-facing feature name.
|
||||
AppWindow.Title = "Quick Accent";
|
||||
|
||||
// The accent popup is shown/hidden instantly (no slide/fade) for typing-aid
|
||||
// responsiveness. TransientSurface defaults to Transition.None (no animation);
|
||||
// SubscribeSurfaceTo forwards to the inner surface so it follows this window's Show/Hide.
|
||||
Selector.SubscribeSurfaceTo(this);
|
||||
|
||||
_powerAccent = new Core.PowerAccent(RunOnUiThread);
|
||||
_powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay;
|
||||
_powerAccent.OnSelectCharacter += PowerAccent_OnSelectCharacter;
|
||||
|
||||
// No manual theme handling: App.xaml leaves RequestedTheme unset, so WinUI follows the system
|
||||
// theme and re-resolves the {ThemeResource} brushes (and retints the acrylic) on a live
|
||||
// light/dark switch, even for this never-activated SW_SHOWNA overlay.
|
||||
}
|
||||
|
||||
// Marshal keyboard-hook callbacks (ShowToolbar / HideToolbar / NextChar) onto the UI thread. The
|
||||
// hook runs on this UI thread, so callbacks arrive here already; run them inline (not via
|
||||
// TryEnqueue, which would defer) so the accent injection stays ordered before the hook returns
|
||||
// and the trigger key-up propagates. Fall back to enqueueing if ever called off-thread.
|
||||
private void RunOnUiThread(Action action)
|
||||
{
|
||||
if (DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() => action());
|
||||
}
|
||||
}
|
||||
|
||||
private void PowerAccent_OnChangeDisplay(bool isActive, string[] chars)
|
||||
{
|
||||
if (!isActive)
|
||||
{
|
||||
_active = false;
|
||||
|
||||
// Release always-on-top before hiding so the dormant overlay does not keep a discrete
|
||||
// GPU awake on hybrid-graphics laptops (issue #34849 / PR #41044). IsAlwaysOnTop is the
|
||||
// WinUIEx WindowEx property (same as the sibling PowerDisplay).
|
||||
IsAlwaysOnTop = false;
|
||||
Hide();
|
||||
ViewModel.Characters.Clear();
|
||||
_selectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
_active = true;
|
||||
ViewModel.ShowDescription = _powerAccent.ShowUnicodeDescription;
|
||||
|
||||
ViewModel.Characters.Clear();
|
||||
foreach (var c in chars)
|
||||
{
|
||||
ViewModel.Characters.Add(c);
|
||||
}
|
||||
|
||||
Selector.SetSelectedIndex(_selectedIndex);
|
||||
ViewModel.Description = (_selectedIndex >= 0 && _selectedIndex < _powerAccent.CharacterDescriptions.Length)
|
||||
? _powerAccent.CharacterDescriptions[_selectedIndex]
|
||||
: string.Empty;
|
||||
|
||||
// Always-on-top only while shown, so the overlay sits above the foreground app (Show uses
|
||||
// SW_SHOWNA and never activates it); released on hide (see above). Then size and show.
|
||||
IsAlwaysOnTop = true;
|
||||
SizeAndPosition();
|
||||
Show();
|
||||
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
if (_active)
|
||||
{
|
||||
Selector.ScrollSelectedIntoView(_selectedIndex);
|
||||
}
|
||||
});
|
||||
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Core.Telemetry.PowerAccentShowAccentMenuEvent());
|
||||
}
|
||||
|
||||
private void PowerAccent_OnSelectCharacter(int index, string character)
|
||||
{
|
||||
_selectedIndex = index;
|
||||
Selector.SetSelectedIndex(index);
|
||||
|
||||
if (index >= 0 && index < _powerAccent.CharacterDescriptions.Length)
|
||||
{
|
||||
ViewModel.Description = _powerAccent.CharacterDescriptions[index];
|
||||
}
|
||||
|
||||
Selector.ScrollSelectedIntoView(index);
|
||||
}
|
||||
|
||||
private void SizeAndPosition()
|
||||
{
|
||||
// Width hugs the content: item count * ItemWidthDip (see the class-level note on why the
|
||||
// ListView is not measured), capped at the monitor's max usable width so long lists scroll.
|
||||
double maxWidthDip = _powerAccent.GetDisplayMaxWidth();
|
||||
double contentWidthDip = ViewModel.Characters.Count * ItemWidthDip;
|
||||
|
||||
// The Unicode description row needs room for a readable line; the WPF original gave it a
|
||||
// 600px MinWidth. Widen a short accent bar to match when the row is shown (the accent bar
|
||||
// itself stays centered within the wider window).
|
||||
if (ViewModel.ShowDescription)
|
||||
{
|
||||
contentWidthDip = Math.Max(contentWidthDip, DescriptionMinWidthDip);
|
||||
}
|
||||
|
||||
double widthDip = Math.Clamp(contentWidthDip, ItemWidthDip, maxWidthDip);
|
||||
double heightDip = RowHeightDip + (ViewModel.ShowDescription ? DescriptionHeightDip : 0);
|
||||
|
||||
// Calculation works in physical pixels; GetDisplayCoordinates multiplies the DIP size by
|
||||
// the active monitor's DPI internally and returns the physical top-left for the anchor.
|
||||
var coordinates = _powerAccent.GetDisplayCoordinates(new CoreSize(widthDip, heightDip));
|
||||
|
||||
var display = DisplayArea.GetFromPoint(
|
||||
new PointInt32((int)Math.Round(coordinates.X), (int)Math.Round(coordinates.Y)),
|
||||
DisplayAreaFallback.Nearest);
|
||||
|
||||
double dpiScale = FlyoutWindowHelper.GetDpiScale(display);
|
||||
|
||||
var rect = new RectInt32(
|
||||
(int)Math.Round(coordinates.X),
|
||||
(int)Math.Round(coordinates.Y),
|
||||
(int)Math.Ceiling(widthDip * dpiScale),
|
||||
(int)Math.Ceiling(heightDip * dpiScale));
|
||||
|
||||
FlyoutWindowHelper.MoveAndResizeOnDisplay(this, display, rect);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_powerAccent.SaveUsageInfo();
|
||||
_powerAccent.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="PowerAccent.UI.SelectorControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<controls:TransientSurface x:Name="Surface" Margin="24,24,24,16">
|
||||
<Grid x:Name="RootContent">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}">
|
||||
<ListView
|
||||
x:Name="CharactersList"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
AutomationProperties.AutomationId="QuickAccentCharacterList"
|
||||
IsHitTestVisible="False"
|
||||
IsItemClickEnabled="False"
|
||||
ItemsSource="{x:Bind ViewModel.Characters, Mode=OneWay}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.HorizontalScrollMode="Enabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="Single">
|
||||
<!--
|
||||
Disable default ListView item animations: the bar is rebuilt on every keystroke,
|
||||
and the built-in slide/fade transitions read as lag on a typing aid.
|
||||
-->
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
<ListView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListView.ItemsPanel>
|
||||
<!--
|
||||
Custom container template reproducing the WPF accent "pill": an inset, rounded,
|
||||
accent-filled rectangle shown only on selection (via VisualStateManager, since
|
||||
WinUI 3 has no Style/ControlTemplate triggers), with the glyph turning on-accent.
|
||||
-->
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<!--
|
||||
WinUI's ListViewItem defaults MinWidth to 88; without pinning it to the
|
||||
48px cell width each glyph is padded out, leaving wide gaps between accents.
|
||||
-->
|
||||
<Setter Property="MinWidth" Value="48" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Grid
|
||||
Height="48"
|
||||
MinWidth="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Border
|
||||
x:Name="SelectionIndicator"
|
||||
Margin="7"
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource AccentControlElevationBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Opacity="0" />
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Margin="12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Selected">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SelectionIndicator.Opacity" Value="1" />
|
||||
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PointerOverSelected">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SelectionIndicator.Opacity" Value="1" />
|
||||
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PressedSelected">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SelectionIndicator.Opacity" Value="1" />
|
||||
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
Text="{x:Bind Mode=OneTime}"
|
||||
TextAlignment="Center" />
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.DescriptionVisibility, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Name="CharacterName"
|
||||
MaxHeight="36"
|
||||
Margin="8"
|
||||
AutomationProperties.AutomationId="QuickAccentDescription"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.Description, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<Rectangle
|
||||
Height="1"
|
||||
VerticalAlignment="Top"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</controls:TransientSurface>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace PowerAccent.UI;
|
||||
|
||||
/// <summary>
|
||||
/// The accent selector content. Hosting it in a UserControl (rather than directly in the
|
||||
/// TransparentWindow) lets x:Bind initialize on the control's Loading pass - which fires when the
|
||||
/// SW_SHOWNA overlay is first laid out - instead of on Window.Activated (which never fires for a
|
||||
/// never-activated overlay). That removes the need to call Bindings.Update() by hand.
|
||||
/// </summary>
|
||||
public sealed partial class SelectorControl : UserControl
|
||||
{
|
||||
public SelectorViewModel ViewModel { get; } = new();
|
||||
|
||||
public SelectorControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// Number of items currently in the accent bar (mirrors the bound ObservableCollection).
|
||||
public int ItemCount => CharactersList.Items.Count;
|
||||
|
||||
// Wire the inner TransientSurface to the hosting window's Show/Hide so it animates in/out.
|
||||
// TransientSurface.SubscribeTo explicitly supports being "placed within" the window content.
|
||||
public void SubscribeSurfaceTo(TransparentWindow host) => Surface.SubscribeTo(host);
|
||||
|
||||
public void SetSelectedIndex(int index) => CharactersList.SelectedIndex = index;
|
||||
|
||||
public void ScrollSelectedIntoView(int index)
|
||||
{
|
||||
if (index >= 0 && index < CharactersList.Items.Count)
|
||||
{
|
||||
CharactersList.ScrollIntoView(CharactersList.Items[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using PowerToys.Interop;
|
||||
|
||||
namespace PowerAccent.UI;
|
||||
@@ -16,13 +15,14 @@ namespace PowerAccent.UI;
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
|
||||
private static App _application;
|
||||
private static Mutex _mutex;
|
||||
private static int _powerToysRunnerPid;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger("\\QuickAccent\\Logs");
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredQuickAccentEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
@@ -30,21 +30,32 @@ internal static class Program
|
||||
return;
|
||||
}
|
||||
|
||||
_mutex = new Mutex(true, "QuickAccent", out bool createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
Logger.LogWarning("Another running QuickAccent instance was detected. Exiting QuickAccent");
|
||||
return;
|
||||
}
|
||||
|
||||
Arguments(args);
|
||||
InitExitListener();
|
||||
|
||||
InitEvents();
|
||||
Microsoft.UI.Xaml.Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
|
||||
_application = new App();
|
||||
_application.InitializeComponent();
|
||||
_application.Run();
|
||||
_mutex?.ReleaseMutex();
|
||||
}
|
||||
|
||||
private static void InitEvents()
|
||||
private static void InitExitListener()
|
||||
{
|
||||
Task.Run(
|
||||
() =>
|
||||
{
|
||||
EventWaitHandle eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.PowerAccentExitEvent());
|
||||
using EventWaitHandle eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.PowerAccentExitEvent());
|
||||
if (eventHandle.WaitOne())
|
||||
{
|
||||
Terminate();
|
||||
@@ -55,39 +66,41 @@ internal static class Program
|
||||
|
||||
private static void Arguments(string[] args)
|
||||
{
|
||||
if (args?.Length > 0)
|
||||
if (args?.Length > 0 && int.TryParse(args[0], out _powerToysRunnerPid))
|
||||
{
|
||||
try
|
||||
Logger.LogInfo($"QuickAccent started from the PowerToys Runner. Runner pid={_powerToysRunnerPid}");
|
||||
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
|
||||
{
|
||||
if (int.TryParse(args[0], out _powerToysRunnerPid))
|
||||
{
|
||||
Logger.LogInfo($"QuickAccent started from the PowerToys Runner. Runner pid={_powerToysRunnerPid}");
|
||||
|
||||
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting QuickAccent");
|
||||
Terminate();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting QuickAccent");
|
||||
Terminate();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"QuickAccent started detached from PowerToys Runner.");
|
||||
Logger.LogInfo("QuickAccent started detached from PowerToys Runner.");
|
||||
_powerToysRunnerPid = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Terminate()
|
||||
{
|
||||
Application.Current.Dispatcher.BeginInvoke(() =>
|
||||
var app = App.Current;
|
||||
var queue = app?.DispatcherQueueForApp;
|
||||
|
||||
// If the exit signal arrives during the brief startup window before OnLaunched has set
|
||||
// DispatcherQueueForApp (e.g. the runner dies, or disable() is called, right after launch),
|
||||
// or the queue is already draining, TryEnqueue can't run our cleanup. Fall back to a hard
|
||||
// exit so we never orphan the process with the low-level keyboard hook still installed. The
|
||||
// OS releases the hook on process termination; usage stats are simply not saved on this path.
|
||||
if (queue is null || !queue.TryEnqueue(() =>
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
Application.Current.Shutdown();
|
||||
});
|
||||
App.Window?.Dispose(); // MainWindow.SaveUsageInfo + Core.PowerAccent.Dispose on the UI thread
|
||||
app.Dispose(); // disposes ETWTrace (idempotent via _disposed guard)
|
||||
app.Exit();
|
||||
}))
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<Window
|
||||
x:Class="PowerAccent.UI.Selector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="MainWindow"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
SizeChanged="Window_SizeChanged"
|
||||
SizeToContent="WidthAndHeight"
|
||||
Visibility="Collapsed"
|
||||
WindowStyle="None"
|
||||
mc:Ignorable="d">
|
||||
<Window.Resources>
|
||||
|
||||
<DataTemplate x:Key="DefaultKeyTemplate">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding}"
|
||||
TextAlignment="Center" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="SelectedKeyTemplate">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="{Binding}"
|
||||
TextAlignment="Center" />
|
||||
</DataTemplate>
|
||||
|
||||
</Window.Resources>
|
||||
<Border
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox
|
||||
x:Name="characters"
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
Focusable="False"
|
||||
IsHitTestVisible="False"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource DefaultKeyTemplate}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ListBoxItem}">
|
||||
<Grid
|
||||
Height="48"
|
||||
MinWidth="48"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
SnapsToDevicePixels="true">
|
||||
<Rectangle
|
||||
x:Name="SelectionIndicator"
|
||||
Margin="7"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource AccentFillColorDefaultBrush}"
|
||||
RadiusX="4"
|
||||
RadiusY="4"
|
||||
Stroke="{DynamicResource AccentControlElevationBorderBrush}"
|
||||
StrokeThickness="1"
|
||||
Visibility="Collapsed" />
|
||||
<ContentPresenter Margin="12" />
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="true">
|
||||
<Setter TargetName="SelectionIndicator" Property="Visibility" Value="Visible" />
|
||||
<Setter Property="ContentTemplate" Value="{StaticResource SelectedKeyTemplate}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
</ListBox>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
MinWidth="600"
|
||||
MaxWidth="{Binding ActualWidth, ElementName=characters}"
|
||||
Background="{DynamicResource LayerOnAcrylicFillColorDefaultBrush}"
|
||||
Visibility="{Binding CharacterNameVisibility, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBlock
|
||||
x:Name="characterName"
|
||||
MaxHeight="36"
|
||||
Margin="8"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="(U+0000) A COOL LETTER NAME COMES HERE"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap" />
|
||||
<Rectangle
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Fill="{DynamicResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -1,185 +0,0 @@
|
||||
// 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;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using Point = PowerAccent.Core.Point;
|
||||
using Size = PowerAccent.Core.Size;
|
||||
|
||||
namespace PowerAccent.UI;
|
||||
|
||||
public partial class Selector : Window, IDisposable, INotifyPropertyChanged
|
||||
{
|
||||
// When setting the position for the selector window, we do not alter the z-order,
|
||||
// activation status, or size.
|
||||
private const SET_WINDOW_POS_FLAGS WindowPosFlags =
|
||||
SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
|
||||
|
||||
private readonly Core.PowerAccent _powerAccent = new();
|
||||
|
||||
private Visibility _characterNameVisibility = Visibility.Visible;
|
||||
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public Visibility CharacterNameVisibility
|
||||
{
|
||||
get
|
||||
{
|
||||
return _characterNameVisibility;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_characterNameVisibility = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CharacterNameVisibility)));
|
||||
}
|
||||
}
|
||||
|
||||
public Selector()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Application.Current.MainWindow.ShowActivated = false;
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
_powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay;
|
||||
_powerAccent.OnSelectCharacter += PowerAccent_OnSelectionCharacter;
|
||||
this.Visibility = Visibility.Hidden;
|
||||
}
|
||||
|
||||
private void PowerAccent_OnSelectionCharacter(int index, string character)
|
||||
{
|
||||
_selectedIndex = index;
|
||||
characters.SelectedIndex = _selectedIndex;
|
||||
|
||||
if (_selectedIndex >= 0 && _selectedIndex < _powerAccent.CharacterDescriptions.Length)
|
||||
{
|
||||
characterName.Text = _powerAccent.CharacterDescriptions[_selectedIndex];
|
||||
}
|
||||
|
||||
if (characters.Items.Count > _selectedIndex && _selectedIndex >= 0)
|
||||
{
|
||||
characters.ScrollIntoView(characters.Items[_selectedIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
private void PowerAccent_OnChangeDisplay(bool isActive, string[] chars)
|
||||
{
|
||||
// Topmost is conditionally set here to address hybrid graphics issues on laptops.
|
||||
this.Topmost = isActive;
|
||||
|
||||
CharacterNameVisibility = _powerAccent.ShowUnicodeDescription ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
int offscreenX = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN) - 1000;
|
||||
int offscreenY = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN) - 1000;
|
||||
|
||||
var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
|
||||
if (hwnd != IntPtr.Zero)
|
||||
{
|
||||
// Move off-screen to avoid flicker on previous monitor before Show() and
|
||||
// UpdateLayout().
|
||||
PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, offscreenX, offscreenY, 0, 0, WindowPosFlags);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Left = offscreenX;
|
||||
this.Top = offscreenY;
|
||||
}
|
||||
|
||||
Show();
|
||||
SetWindowsSize();
|
||||
characters.ItemsSource = chars;
|
||||
characters.SelectedIndex = -1; // Reset before setting dynamically to avoid flashing
|
||||
|
||||
this.UpdateLayout(); // Required for filling the actual width/height before positioning.
|
||||
|
||||
characters.SelectedIndex = _selectedIndex;
|
||||
|
||||
if (_selectedIndex >= 0 && _selectedIndex < chars.Length)
|
||||
{
|
||||
characterName.Text = _powerAccent.CharacterDescriptions[_selectedIndex];
|
||||
characters.ScrollIntoView(characters.Items[_selectedIndex]);
|
||||
this.UpdateLayout(); // Re-layout after scrolling
|
||||
}
|
||||
else
|
||||
{
|
||||
characterName.Text = string.Empty;
|
||||
}
|
||||
|
||||
SetWindowPosition();
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new PowerAccent.Core.Telemetry.PowerAccentShowAccentMenuEvent());
|
||||
}
|
||||
else
|
||||
{
|
||||
Hide();
|
||||
characters.ItemsSource = null;
|
||||
_selectedIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void MenuExit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
private void SetWindowPosition()
|
||||
{
|
||||
Size windowSize = new(((FrameworkElement)Application.Current.MainWindow.Content).ActualWidth, ((FrameworkElement)Application.Current.MainWindow.Content).ActualHeight);
|
||||
Point physicalPosition = _powerAccent.GetDisplayCoordinates(windowSize);
|
||||
|
||||
var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
|
||||
if (hwnd != IntPtr.Zero)
|
||||
{
|
||||
PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, (int)Math.Round(physicalPosition.X), (int)Math.Round(physicalPosition.Y), 0, 0, WindowPosFlags);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
|
||||
{
|
||||
base.OnDpiChanged(oldDpi, newDpi);
|
||||
if (this.Visibility == Visibility.Visible)
|
||||
{
|
||||
SetWindowsSize();
|
||||
SetWindowPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetWindowsSize()
|
||||
{
|
||||
double maxWidth = _powerAccent.GetDisplayMaxWidth();
|
||||
this.characters.MaxWidth = maxWidth;
|
||||
this.MaxWidth = maxWidth;
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (this.Visibility == Visibility.Visible)
|
||||
{
|
||||
SetWindowPosition();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_powerAccent.SaveUsageInfo();
|
||||
_powerAccent.Dispose();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
36
src/modules/poweraccent/PowerAccent.UI/SelectorViewModel.cs
Normal file
36
src/modules/poweraccent/PowerAccent.UI/SelectorViewModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace PowerAccent.UI;
|
||||
|
||||
public partial class SelectorViewModel : ObservableObject
|
||||
{
|
||||
// Partial properties (not [ObservableProperty] fields): the CsWinRT generators need partial
|
||||
// properties to emit correct WinRT marshalling for a WinUI 3 app (otherwise MVVMTK0045).
|
||||
// Partial properties cannot carry field initializers, so initial values are set in the ctor.
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<string> Characters { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Description { get; set; }
|
||||
|
||||
// Exposed directly as a Visibility (rather than binding the bool through a
|
||||
// BoolToVisibilityConverter) so the description row's visibility needs no converter resource.
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DescriptionVisibility))]
|
||||
public partial bool ShowDescription { get; set; }
|
||||
|
||||
public SelectorViewModel()
|
||||
{
|
||||
Characters = new ObservableCollection<string>();
|
||||
Description = string.Empty;
|
||||
}
|
||||
|
||||
public Visibility DescriptionVisibility => ShowDescription ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
@@ -96,6 +96,11 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_settings.inputTime = std::chrono::milliseconds(inputTime);
|
||||
}
|
||||
|
||||
void KeyboardListener::UpdateHoldDuration(int32_t holdDuration)
|
||||
{
|
||||
m_settings.holdDuration = std::chrono::milliseconds(holdDuration);
|
||||
}
|
||||
|
||||
void KeyboardListener::UpdateExcludedApps(std::wstring_view excludedAppsView)
|
||||
{
|
||||
std::vector<std::wstring> excludedApps;
|
||||
@@ -123,6 +128,17 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
return m_settings.doNotActivateOnGameMode && detect_game_mode();
|
||||
}
|
||||
|
||||
bool KeyboardListener::IsBlockingModifierDown()
|
||||
{
|
||||
// Ctrl / Alt (including AltGr = Ctrl+Alt) / Win turn a held letter into a shortcut,
|
||||
// so they must not trigger press-and-hold. Shift is intentionally allowed so that
|
||||
// uppercase accents still work.
|
||||
return (GetAsyncKeyState(VK_CONTROL) & 0x8000) ||
|
||||
(GetAsyncKeyState(VK_MENU) & 0x8000) ||
|
||||
(GetAsyncKeyState(VK_LWIN) & 0x8000) ||
|
||||
(GetAsyncKeyState(VK_RWIN) & 0x8000);
|
||||
}
|
||||
|
||||
bool KeyboardListener::IsForegroundAppExcluded()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex_excluded_apps);
|
||||
@@ -181,6 +197,25 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
letterPressed = letterKey;
|
||||
}
|
||||
|
||||
// Press-and-hold activation: the held letter itself opens the toolbar after the hold
|
||||
// duration. The base letter still types on first press; auto-repeats are swallowed above.
|
||||
if (m_settings.activationKey == PowerAccentActivationKey::PressAndHold &&
|
||||
!m_toolbarVisible &&
|
||||
letterPressed != LetterKey::None &&
|
||||
letterKey == letterPressed &&
|
||||
!IsBlockingModifierDown() &&
|
||||
!IsSuppressedByGameMode() &&
|
||||
!IsForegroundAppExcluded())
|
||||
{
|
||||
Logger::debug(L"Show toolbar (press-and-hold). Letter: {}", letterPressed);
|
||||
m_triggeredWithSpace = false;
|
||||
m_triggeredWithLeftArrow = false;
|
||||
m_triggeredWithRightArrow = false;
|
||||
m_toolbarVisible = true;
|
||||
m_showToolbarCb(letterPressed);
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT triggerPressed = 0;
|
||||
if (letterPressed != LetterKey::None)
|
||||
{
|
||||
@@ -199,7 +234,9 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
|
||||
// Trigger-key activation (letter + Space/arrow) is exclusive to the non-hold modes.
|
||||
if (m_settings.activationKey != PowerAccentActivationKey::PressAndHold &&
|
||||
!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
|
||||
{
|
||||
Logger::debug(L"Show toolbar. Letter: {}, Trigger: {}", letterPressed, triggerPressed);
|
||||
|
||||
@@ -211,7 +248,14 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_showToolbarCb(letterPressed);
|
||||
}
|
||||
|
||||
if (m_toolbarVisible && triggerPressed)
|
||||
// In press-and-hold the popup only appears once the hold duration elapses, so Space/arrow
|
||||
// must pass through until then; treat the picker as interactive only once it is shown.
|
||||
const bool pickerInteractive =
|
||||
m_toolbarVisible &&
|
||||
(m_settings.activationKey != PowerAccentActivationKey::PressAndHold ||
|
||||
m_stopwatch.elapsed() >= m_settings.holdDuration);
|
||||
|
||||
if (pickerInteractive && triggerPressed)
|
||||
{
|
||||
if (triggerPressed == VK_LEFT)
|
||||
{
|
||||
@@ -247,13 +291,27 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_rightShiftPressed = false;
|
||||
}
|
||||
|
||||
if (std::find(std::begin(letters), end(letters), static_cast<LetterKey>(info.vkCode)) != end(letters) && m_isLanguageLetterCb(static_cast<LetterKey>(info.vkCode)))
|
||||
const auto releasedLetter = static_cast<LetterKey>(info.vkCode);
|
||||
if (std::find(std::begin(letters), end(letters), releasedLetter) != end(letters) && m_isLanguageLetterCb(releasedLetter))
|
||||
{
|
||||
// Only react to the key-up of the letter that owns the toolbar, so releasing a
|
||||
// different held letter can't cancel or commit the active picker.
|
||||
if (letterPressed != releasedLetter)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
letterPressed = LetterKey::None;
|
||||
|
||||
if (m_toolbarVisible)
|
||||
{
|
||||
if (m_stopwatch.elapsed() < m_settings.inputTime)
|
||||
// Press-and-hold uses its own (typically longer) hold duration as the
|
||||
// minimum-hold threshold; the trigger-key modes use inputTime.
|
||||
const auto activationThreshold =
|
||||
m_settings.activationKey == PowerAccentActivationKey::PressAndHold
|
||||
? m_settings.holdDuration
|
||||
: m_settings.inputTime;
|
||||
if (m_stopwatch.elapsed() < activationThreshold)
|
||||
{
|
||||
Logger::debug(L"Activation too fast. Do nothing.");
|
||||
|
||||
@@ -275,7 +333,11 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_hideToolbarCb(InputType::None);
|
||||
}
|
||||
m_toolbarVisible = false;
|
||||
return true;
|
||||
|
||||
// In press-and-hold the base letter already typed on key-down and no trigger
|
||||
// key was consumed, so let this key-up pass through to avoid a stuck-key
|
||||
// perception. Trigger modes keep swallowing it as before.
|
||||
return m_settings.activationKey != PowerAccentActivationKey::PressAndHold;
|
||||
}
|
||||
Logger::debug(L"Hide toolbar event and input char");
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
LeftRightArrow,
|
||||
Space,
|
||||
Both,
|
||||
PressAndHold,
|
||||
};
|
||||
|
||||
struct PowerAccentSettings
|
||||
@@ -18,6 +19,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
PowerAccentActivationKey activationKey{ PowerAccentActivationKey::Both };
|
||||
bool doNotActivateOnGameMode{ true };
|
||||
std::chrono::milliseconds inputTime{ 300 }; // Should match with UI.Library.PowerAccentSettings.DefaultInputTimeMs
|
||||
std::chrono::milliseconds holdDuration{ 500 }; // Should match with UI.Library.PowerAccentSettings.DefaultHoldDurationMs
|
||||
std::vector<std::wstring> excludedApps;
|
||||
};
|
||||
|
||||
@@ -39,6 +41,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
void UpdateActivationKey(int32_t activationKey);
|
||||
void UpdateDoNotActivateOnGameMode(bool doNotActivateOnGameMode);
|
||||
void UpdateInputTime(int32_t inputTime);
|
||||
void UpdateHoldDuration(int32_t holdDuration);
|
||||
void UpdateExcludedApps(std::wstring_view excludedApps);
|
||||
|
||||
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
|
||||
@@ -48,6 +51,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
bool OnKeyUp(KBDLLHOOKSTRUCT info) noexcept;
|
||||
bool IsSuppressedByGameMode();
|
||||
bool IsForegroundAppExcluded();
|
||||
bool IsBlockingModifierDown();
|
||||
|
||||
static inline KeyboardListener* s_instance;
|
||||
HHOOK s_llKeyboardHook = nullptr;
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace PowerToys
|
||||
void UpdateActivationKey(Int32 activationKey);
|
||||
void UpdateDoNotActivateOnGameMode(Boolean doNotActivateOnGameMode);
|
||||
void UpdateInputTime(Int32 inputTime);
|
||||
void UpdateHoldDuration(Int32 holdDuration);
|
||||
void UpdateExcludedApps(String excludedApps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<TargetName>PowerToys.PowerAccentKeyboardService</TargetName>
|
||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\</OutDir>
|
||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
|
||||
@@ -59,7 +59,7 @@ private:
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
std::wstring executable_args = L"" + std::to_wstring(powertoys_pid);
|
||||
std::wstring application_path = L"PowerToys.PowerAccent.exe";
|
||||
std::wstring application_path = L"WinUI3Apps\\PowerToys.PowerAccent.exe";
|
||||
std::wstring full_command_path = application_path + L" " + executable_args.data();
|
||||
Logger::trace(L"PowerToys QuickAccent launching: " + full_command_path);
|
||||
|
||||
|
||||
@@ -48,6 +48,13 @@ namespace RegistryPreview
|
||||
SetTitleBar(titleBar);
|
||||
AppWindow.SetIcon("Assets\\RegistryPreview\\RegistryPreview.ico");
|
||||
|
||||
// Ensure a non-empty window title before the title bar's first layout reads it.
|
||||
// UpdateWindowTitle() only runs later (on file load), so without this the native
|
||||
// window title would be empty during startup, which can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
titleBar.Title = APPNAME;
|
||||
AppWindow.Title = APPNAME;
|
||||
|
||||
// if have settings, update the location of the window
|
||||
if (jsonWindowPlacement != null)
|
||||
{
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations
|
||||
LeftRightArrow,
|
||||
Space,
|
||||
Both,
|
||||
PressAndHold,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
AlwaysRunNotElevated = new BoolProperty(true);
|
||||
AlwaysOnTop = new BoolProperty(false);
|
||||
ShowTaskbarIcon = new BoolProperty(true);
|
||||
CloseAfterLosingFocus = new BoolProperty(false);
|
||||
ConfirmFileDelete = new BoolProperty(true);
|
||||
EnableSpaceToActivate = new BoolProperty(true); // Toggle is ON by default for new users. No impact on existing users.
|
||||
@@ -27,6 +29,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public BoolProperty AlwaysRunNotElevated { get; set; }
|
||||
|
||||
public BoolProperty AlwaysOnTop { get; set; }
|
||||
|
||||
public BoolProperty ShowTaskbarIcon { get; set; }
|
||||
|
||||
public BoolProperty CloseAfterLosingFocus { get; set; }
|
||||
|
||||
public BoolProperty ConfirmFileDelete { get; set; }
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("input_time_ms")]
|
||||
public IntProperty InputTime { get; set; }
|
||||
|
||||
[JsonPropertyName("hold_duration_ms")]
|
||||
public IntProperty HoldDuration { get; set; }
|
||||
|
||||
[JsonPropertyName("selected_lang")]
|
||||
public StringProperty SelectedLang { get; set; }
|
||||
|
||||
@@ -43,6 +46,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
DoNotActivateOnGameMode = true;
|
||||
ToolbarPosition = "Top center";
|
||||
InputTime = new IntProperty(PowerAccentSettings.DefaultInputTimeMs);
|
||||
HoldDuration = new IntProperty(PowerAccentSettings.DefaultHoldDurationMs);
|
||||
SelectedLang = "ALL";
|
||||
ExcludedApps = new StringProperty();
|
||||
ShowUnicodeDescription = false;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public const string ModuleName = "QuickAccent";
|
||||
public const string ModuleVersion = "0.0.1";
|
||||
public const int DefaultInputTimeMs = 300; // PowerAccentKeyboardService.PowerAccentSettings.inputTime should be the same
|
||||
public const int DefaultHoldDurationMs = 500; // PowerAccentKeyboardService.PowerAccentSettings.holdDuration should be the same
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public PowerAccentProperties Properties { get; set; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -45,7 +45,17 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(titleBar);
|
||||
|
||||
this.Title = resourceLoader.GetString("ShortcutConflictWindow_Title");
|
||||
var windowTitle = resourceLoader.GetString("ShortcutConflictWindow_Title");
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(windowTitle))
|
||||
{
|
||||
windowTitle = "PowerToys shortcut conflicts";
|
||||
}
|
||||
|
||||
this.Title = windowTitle;
|
||||
this.CenterOnScreen();
|
||||
|
||||
ViewModel.OnPageLoaded();
|
||||
|
||||
@@ -46,6 +46,18 @@
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.AlwaysRunNotElevated, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PeekAlwaysOnTop"
|
||||
x:Uid="Peek_AlwaysOnTop"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.AlwaysOnTop, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PeekShowTaskbarIcon"
|
||||
x:Uid="Peek_ShowTaskbarIcon"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.ShowTaskbarIcon, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PeekCloseAfterLosingFocus"
|
||||
x:Uid="Peek_CloseAfterLosingFocus"
|
||||
|
||||
@@ -45,8 +45,22 @@
|
||||
<ComboBoxItem x:Uid="QuickAccent_Activation_Key_Arrows" />
|
||||
<ComboBoxItem x:Uid="QuickAccent_Activation_Key_Space" />
|
||||
<ComboBoxItem x:Uid="QuickAccent_Activation_Key_Either" />
|
||||
<ComboBoxItem x:Uid="QuickAccent_Activation_Key_PressAndHold" />
|
||||
</ComboBox>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="QuickAccent_HoldDurationMs"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
Visibility="{x:Bind ViewModel.IsPressAndHoldActivation, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="100"
|
||||
Maximum="3000"
|
||||
Minimum="100"
|
||||
SmallChange="10"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.HoldDurationMs, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left">
|
||||
<CheckBox x:Uid="QuickAccent_Prevent_Activation_On_Game_Mode" IsChecked="{x:Bind ViewModel.DoNotActivateOnGameMode, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
@@ -191,7 +205,8 @@
|
||||
<tkcontrols:SettingsCard
|
||||
Name="QuickAccentInputTimeMs"
|
||||
x:Uid="QuickAccent_InputTimeMs"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
Visibility="{x:Bind ViewModel.IsPressAndHoldActivation, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="100"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -3156,6 +3156,14 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Runs Peek without admin permissions to improve access to network shares. To apply this change, you must disable and re-enable Peek.</value>
|
||||
<comment>Peek is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Peek_AlwaysOnTop.Header" xml:space="preserve">
|
||||
<value>Peek window is always on top</value>
|
||||
<comment>Peek is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Peek_ShowTaskbarIcon.Header" xml:space="preserve">
|
||||
<value>Show Peek icon on the taskbar</value>
|
||||
<comment>Peek is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Peek_CloseAfterLosingFocus.Header" xml:space="preserve">
|
||||
<value>Automatically close the Peek window after it loses focus</value>
|
||||
<comment>Peek is a product name, do not loc</comment>
|
||||
@@ -3354,8 +3362,8 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<comment>key refers to a physical key on a keyboard</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_Activation_Shortcut.Description" xml:space="preserve">
|
||||
<value>Press this key after holding down the target letter</value>
|
||||
<comment>key refers to a physical key on a keyboard</comment>
|
||||
<value>Choose how the accent menu opens</value>
|
||||
<comment>The accent menu is the Quick Accent character picker</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_Activation_Key_Arrows.Content" xml:space="preserve">
|
||||
<value>Left/Right Arrow</value>
|
||||
@@ -3369,6 +3377,10 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>Left, Right or Space</value>
|
||||
<comment>All are keys on a keyboard</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_Activation_Key_PressAndHold.Content" xml:space="preserve">
|
||||
<value>Press and hold the letter</value>
|
||||
<comment>Activation mode where holding the letter key itself opens the accent menu, like on iOS or macOS</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_Toolbar.Header" xml:space="preserve">
|
||||
<value>Toolbar</value>
|
||||
</data>
|
||||
@@ -3413,6 +3425,14 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>How long a key must be held before the accent menu appears</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_HoldDurationMs.Header" xml:space="preserve">
|
||||
<value>Hold duration (ms)</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_HoldDurationMs.Description" xml:space="preserve">
|
||||
<value>How long to hold the letter before the accent menu appears</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
</data>
|
||||
<data name="QuickAccent_ExcludedApps.Description" xml:space="preserve">
|
||||
<value>Prevents module activation if a foreground application is excluded. Add one application name per line.</value>
|
||||
</data>
|
||||
@@ -4942,11 +4962,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>
|
||||
|
||||
@@ -748,6 +748,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
case Library.Enumerations.PowerAccentActivationKey.LeftRightArrow: activation = resourceLoader.GetString("QuickAccent_Activation_Key_Arrows/Content"); break;
|
||||
case Library.Enumerations.PowerAccentActivationKey.Space: activation = resourceLoader.GetString("QuickAccent_Activation_Key_Space/Content"); break;
|
||||
case Library.Enumerations.PowerAccentActivationKey.Both: activation = resourceLoader.GetString("QuickAccent_Activation_Key_Either/Content"); break;
|
||||
case Library.Enumerations.PowerAccentActivationKey.PressAndHold: activation = resourceLoader.GetString("QuickAccent_Activation_Key_PressAndHold/Content"); break;
|
||||
default: activation = string.Empty; break;
|
||||
}
|
||||
|
||||
var list = new List<DashboardModuleItem>
|
||||
|
||||
@@ -197,6 +197,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool AlwaysOnTop
|
||||
{
|
||||
get => _peekSettings.Properties.AlwaysOnTop.Value;
|
||||
set
|
||||
{
|
||||
if (_peekSettings.Properties.AlwaysOnTop.Value != value)
|
||||
{
|
||||
_peekSettings.Properties.AlwaysOnTop.Value = value;
|
||||
OnPropertyChanged(nameof(AlwaysOnTop));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowTaskbarIcon
|
||||
{
|
||||
get => _peekSettings.Properties.ShowTaskbarIcon.Value;
|
||||
set
|
||||
{
|
||||
if (_peekSettings.Properties.ShowTaskbarIcon.Value != value)
|
||||
{
|
||||
_peekSettings.Properties.ShowTaskbarIcon.Value = value;
|
||||
OnPropertyChanged(nameof(ShowTaskbarIcon));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CloseAfterLosingFocus
|
||||
{
|
||||
get => _peekSettings.Properties.CloseAfterLosingFocus.Value;
|
||||
|
||||
@@ -88,6 +88,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
_inputTimeMs = _powerAccentSettings.Properties.InputTime.Value;
|
||||
|
||||
_holdDurationMs = _powerAccentSettings.Properties.HoldDuration.Value;
|
||||
|
||||
_excludedApps = _powerAccentSettings.Properties.ExcludedApps.Value;
|
||||
|
||||
var selectedLangEntries = _powerAccentSettings.Properties.SelectedLang.Value
|
||||
@@ -220,12 +222,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
if (value != (int)_powerAccentSettings.Properties.ActivationKey)
|
||||
{
|
||||
_powerAccentSettings.Properties.ActivationKey = (PowerAccentActivationKey)value;
|
||||
OnPropertyChanged(nameof(ActivationKey));
|
||||
|
||||
// RaisePropertyChanged() re-raises ActivationKey (CallerMemberName) and persists via IPC.
|
||||
OnPropertyChanged(nameof(IsPressAndHoldActivation));
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPressAndHoldActivation => (PowerAccentActivationKey)ActivationKey == PowerAccentActivationKey.PressAndHold;
|
||||
|
||||
public bool DoNotActivateOnGameMode
|
||||
{
|
||||
get
|
||||
@@ -265,6 +271,26 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private int _holdDurationMs = PowerAccentSettings.DefaultHoldDurationMs;
|
||||
|
||||
public int HoldDurationMs
|
||||
{
|
||||
get
|
||||
{
|
||||
return _holdDurationMs;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _holdDurationMs)
|
||||
{
|
||||
_holdDurationMs = value;
|
||||
_powerAccentSettings.Properties.HoldDuration.Value = value;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _excludedApps;
|
||||
|
||||
public string ExcludedApps
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30225.117
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CleanUp_tool", "CleanUp_tool.vcxproj", "{0995F59A-5074-42E4-AFE4-6335A0368E8E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0995F59A-5074-42E4-AFE4-6335A0368E8E}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0995F59A-5074-42E4-AFE4-6335A0368E8E}.Debug|x64.Build.0 = Debug|x64
|
||||
{0995F59A-5074-42E4-AFE4-6335A0368E8E}.Release|x64.ActiveCfg = Release|x64
|
||||
{0995F59A-5074-42E4-AFE4-6335A0368E8E}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B025F9FC-0262-4669-BADD-BBD61121FBD3}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{0995f59a-5074-42e4-afe4-6335a0368e8e}</ProjectGuid>
|
||||
<RootNamespace>CleanUptool</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,169 +0,0 @@
|
||||
#include <windows.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <shlwapi.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
static wchar_t szWindowClass[] = L"CleanUp tool";
|
||||
static wchar_t szTitle[] = L"Tool to clean up FancyZones installation";
|
||||
|
||||
HINSTANCE hInst;
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
|
||||
void CleanUp();
|
||||
void RemoveSettingsFolder();
|
||||
void ClearRegistry();
|
||||
|
||||
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
|
||||
{
|
||||
WNDCLASSEX wcex;
|
||||
|
||||
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||
wcex.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wcex.lpfnWndProc = WndProc;
|
||||
wcex.cbClsExtra = 0;
|
||||
wcex.cbWndExtra = 0;
|
||||
wcex.hInstance = hInstance;
|
||||
wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
|
||||
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||
wcex.lpszMenuName = nullptr;
|
||||
wcex.lpszClassName = szWindowClass;
|
||||
wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION);
|
||||
|
||||
if (!RegisterClassEx(&wcex))
|
||||
{
|
||||
MessageBox(nullptr, L"Call to RegisterClassEx failed!", szTitle, NULL);
|
||||
return 1;
|
||||
}
|
||||
|
||||
hInst = hInstance;
|
||||
|
||||
HWND hWnd = CreateWindow(
|
||||
szWindowClass,
|
||||
szTitle,
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
200, 200,
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (!hWnd)
|
||||
{
|
||||
MessageBox(nullptr, L"Call to CreateWindow failed!", szTitle, NULL);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ShowWindow(hWnd, nCmdShow);
|
||||
UpdateWindow(hWnd);
|
||||
|
||||
HWND hwndButton = CreateWindow(
|
||||
L"BUTTON", // Predefined class; Unicode assumed
|
||||
L"Clear", // Button text
|
||||
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, // Styles
|
||||
50, // x position
|
||||
50, // y position
|
||||
100, // Button width
|
||||
100, // Button height
|
||||
hWnd, // Parent window
|
||||
(HMENU) 1, // No menu.
|
||||
(HINSTANCE)GetWindowLongPtr(hWnd, GWLP_HINSTANCE),
|
||||
nullptr); // Pointer not needed.
|
||||
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
return (int)msg.wParam;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc;
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case WM_PAINT:
|
||||
hdc = BeginPaint(hWnd, &ps);
|
||||
EndPaint(hWnd, &ps);
|
||||
break;
|
||||
case WM_COMMAND:
|
||||
if (wParam == 1)
|
||||
{
|
||||
CleanUp();
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CleanUp()
|
||||
{
|
||||
RemoveSettingsFolder();
|
||||
ClearRegistry();
|
||||
}
|
||||
|
||||
void RemoveSettingsFolder()
|
||||
{
|
||||
wchar_t settingsPath[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPath(nullptr, ssfLOCALAPPDATA, nullptr, 0, settingsPath)))
|
||||
{
|
||||
PathAppend(settingsPath, L"\\Microsoft\\PowerToys");
|
||||
}
|
||||
|
||||
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IFileOperation* pfo;
|
||||
hr = CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&pfo));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hr = pfo->SetOperationFlags(FOF_NO_UI);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
IShellItem* psiFrom = nullptr;
|
||||
hr = SHCreateItemFromParsingName(settingsPath, nullptr, IID_PPV_ARGS(&psiFrom));
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
hr = pfo->DeleteItem(psiFrom, nullptr);
|
||||
}
|
||||
psiFrom->Release();
|
||||
}
|
||||
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
hr = pfo->PerformOperations();
|
||||
}
|
||||
}
|
||||
pfo->Release();
|
||||
}
|
||||
|
||||
void ClearRegistry()
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\SuperFancyZones");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerRename");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\DontShowMeThisDialogAgain\\{e16ea82f-6d94-4f30-bb02-d6d911588afd}");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\ImageResizer");
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#CleanUp tool 1.0
|
||||
#Copyright (C) 2022 Microsoft Corporation
|
||||
#Tool to clean PowerToys settings inside AppData folder and registry
|
||||
|
||||
#Deleting json settings files in %AppData%/Local/Microsoft/PowerToys.
|
||||
|
||||
[String]$SettingsPath = $Env:LOCALAPPDATA + '\Microsoft\PowerToys'
|
||||
|
||||
if (Test-Path -Path $SettingsPath -PathType Any)
|
||||
{
|
||||
Remove-Item -Path $SettingsPath -Recurse
|
||||
}
|
||||
|
||||
#Deleting SuperFancyZones registry key
|
||||
|
||||
[String]$SuperFancyZones = "HKCU:\Software\SuperFancyZones"
|
||||
|
||||
if (Test-Path -Path $SuperFancyZones -PathType Any)
|
||||
{
|
||||
Remove-Item -Path $SuperFancyZones -Recurse
|
||||
}
|
||||
|
||||
#Deleting PowerRename registry key
|
||||
|
||||
[String]$PowerRename = "HKCU:\Software\Microsoft\PowerRename"
|
||||
|
||||
if (Test-Path -Path $PowerRename -PathType Any)
|
||||
{
|
||||
Remove-Item -Path $PowerRename -Recurse
|
||||
}
|
||||
|
||||
#Deleting ImageResizer registry key
|
||||
|
||||
[String]$ImageResizer = "HKCU:\Software\Microsoft\ImageResizer"
|
||||
|
||||
if (Test-Path -Path $ImageResizer -PathType Any)
|
||||
{
|
||||
Remove-Item -Path $ImageResizer -Recurse
|
||||
}
|
||||
|
||||
#Deleting DontShowThisDialogAgain registry key
|
||||
|
||||
[String]$DontShowThisDialogAgain = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\DontShowMeThisDialogAgain\{e16ea82f-6d94-4f30-bb02-d6d911588afd}"
|
||||
|
||||
if (Test-Path -Path $DontShowThisDialogAgain -PathType Any)
|
||||
{
|
||||
Remove-Item -Path $DontShowThisDialogAgain
|
||||
}
|
||||
@@ -432,12 +432,9 @@ function Test-CoreFiles {
|
||||
'PowerToys.MouseWithoutBordersHelper.dll',
|
||||
'PowerToys.MouseWithoutBordersHelper.exe',
|
||||
|
||||
# PowerAccent
|
||||
'PowerAccent.Core.dll',
|
||||
'PowerToys.PowerAccent.dll',
|
||||
'PowerToys.PowerAccent.exe',
|
||||
# PowerAccent - only the runner-loaded module interface ships in the install root.
|
||||
# The app, core, common and keyboard-service binaries moved to WinUI3Apps (see $winUI3SignedFiles).
|
||||
'PowerToys.PowerAccentModuleInterface.dll',
|
||||
'PowerToys.PowerAccentKeyboardService.dll',
|
||||
|
||||
# Workspaces
|
||||
'PowerToys.WorkspacesSnapshotTool.exe',
|
||||
@@ -500,7 +497,14 @@ function Test-CoreFiles {
|
||||
'PowerToys.RegistryPreviewExt.dll',
|
||||
'PowerToys.RegistryPreviewUILib.dll',
|
||||
'PowerToys.RegistryPreview.dll',
|
||||
'PowerToys.RegistryPreview.exe'
|
||||
'PowerToys.RegistryPreview.exe',
|
||||
|
||||
# PowerAccent (Quick Accent) - moved from the install root to WinUI3Apps
|
||||
'PowerAccent.Core.dll',
|
||||
'PowerAccent.Common.dll',
|
||||
'PowerToys.PowerAccent.dll',
|
||||
'PowerToys.PowerAccent.exe',
|
||||
'PowerToys.PowerAccentKeyboardService.dll'
|
||||
)
|
||||
|
||||
# Tools signed files (in Tools subdirectory)
|
||||
|
||||
Reference in New Issue
Block a user