Compare commits

...

16 Commits

Author SHA1 Message Date
Yu Leng (from Dev Box)
ab99b6faa5 [KBM] Make orphaned-key warning a non-blocking notice
The orphaned-key warning previously blocked saving and required a second
Save click to confirm, which is unintuitive in the new per-mapping flow.
Change it to save in a single click and show a dismissible informational
banner on the main page afterwards (only for single-key remaps that leave
the original key with no assignment), so the operation is no longer
interrupted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 19:25:24 +08:00
Yu Leng (from Dev Box)
3197ae1221 [KBM] Fix exact-match and orphaned-key issues found in review
- EnableShortcut: re-enabling a multi-key text mapping went through the
  string AddShortcutMapping overload without exactMatch, silently
  clearing exact-match in the engine while editorSettings.json kept it.
  Now forwards exactMatch.

- UnifiedMappingControl: the exact-match (and app-specific) checkbox
  enable state was only refreshed on keyboard-hook capture, not on
  dropdown key edits / auto-grow. Refresh it from _triggerKeys
  CollectionChanged so changing the trigger via the dropdown keeps the
  state in sync.

- ValidationHelper.IsKeyOrphaned: only count RemapShortcut (key-output)
  mappings as restoring a key; program/URL/text targets do not make a
  physical key reachable again.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 17:48:27 +08:00
Yu Leng (from Dev Box)
b7556c835f [KBM] Add exact match and orphaned-key warning to new WinUI3 editor
Close two functional-regression gaps in the new Keyboard Manager editor
(KeyboardManagerEditorUI) relative to the classic C++ editor:

- Exact Match: thread the origin-shortcut exactMatch flag through the
  native wrapper (AddShortcutRemap + ShortcutMapping struct), interop,
  and UI. Wired for all action types (key/shortcut, text, URL, program,
  disable) on multi-key shortcuts, with save/load/edit round-trip. The
  engine (MappingConfiguration) already serialized exactMatch.

- Orphaned-key warning: wire up the previously-unused
  ValidationHelper.IsKeyOrphaned to warn (via a key-scoped, two-step
  InfoBar confirmation) when a single-key remap leaves the original key
  unassigned, mirroring the classic Remap Keys editor. The check runs
  before deleting the existing mapping so cancelling is non-destructive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 16:05:30 +08:00
moooyo
d6319516d0 [Skills] Fix wpf-to-winui3-migration SKILL.md failing to load (#49059)
## Summary of the Pull Request

The `wpf-to-winui3-migration` agent skill failed to load. The
`description` field in its `SKILL.md` YAML frontmatter was an
**unquoted** plain scalar containing `Keywords: ` (a colon followed by a
space). YAML interprets `: ` as a mapping key/value separator, so the
skill loader failed with:

> failed to parse YAML frontmatter: mapping values are not allowed in
this context at line 2 column 651

Because `.claude/skills` is a symlink to `.github/skills`, the CLI
enumerates the same file twice, so this single defect surfaced as
**two** skill load errors (`skill_error_count: 2`).

Fix: wrap the `description` value in single quotes so the colon is
treated literally. No wording changes; the description stays 926
characters (well under the 1024 limit).

## PR Checklist

- [ ] Closes: #xxx — N/A, trivial metadata fix, no tracking issue
- [x] **Communication:** Metadata-only fix; no design discussion needed
- [ ] **Tests:** N/A — no test harness for skill frontmatter; validated
by YAML parsing (see below)
- [x] **Localization:** N/A — not end-user-facing
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** N/A

## Detailed Description of the Pull Request / Additional comments

`.github/skills/wpf-to-winui3-migration/SKILL.md` line 3 changed from:

```yaml
description: Guide for migrating ... after migration. Keywords: WPF, WinUI, ... SoftwareBitmap.
```

to:

```yaml
description: 'Guide for migrating ... after migration. Keywords: WPF, WinUI, ... SoftwareBitmap.'
```

The three other top-level skills already quote (or avoid `: ` in) their
descriptions, so only this one was affected. Single quotes are used
because the description contains no quote characters, so no escaping is
required.

## Validation Steps Performed

- Reproduced the loader error from the Copilot CLI logs: `mapping values
are not allowed in this context at line 2 column 651`.
- Parsed the frontmatter of all 9 `SKILL.md` files with a YAML parser:
before = 1 failure (this file), after = **0 failures**.
- Confirmed parsed `name` (`wpf-to-winui3-migration`), `description`
(926 chars, ≤ 1024), and `license` are intact and the literal `Keywords:
WPF...` text is preserved.

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 10:48:23 +08:00
Alex Mihaiuc
53737cbe31 ZoomIt add snip and panorama save hotkeys (#49075)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

- Fixes #45808 — ZoomIt's "Snip Save" hotkey was auto-derived by XOR'ing
the Shift modifier from the Snip hotkey, causing `Ctrl+S` to be stolen
when Snip was set to `Ctrl+Shift+S`
- The Snip Save hotkey is now a separate, independently configurable
setting (default: `Ctrl+Shift+6`, complementing the default Snip hotkey
`Ctrl+6`)
- Updated both the PowerToys Settings UI and the standalone ZoomIt
options dialog
- The same is applied for the scrolling screenshot keys

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

ZoomIt registered two hotkeys for Snip — the primary (`SNIP_HOTKEY`) and
a "save to file" variant (`SNIP_SAVE_HOTKEY`) derived by toggling the
Shift modifier via XOR:

```cpp
RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF);
```

This worked fine for the default `Ctrl+6` (save became `Ctrl+Shift+6`),
but when a user configured `Ctrl+Shift+S` as their Snip hotkey, the XOR
removed Shift, making the save variant `Ctrl+S` — stealing a ubiquitous
shortcut from every other application.

## Changes

### C++ Backend (ZoomIt core)

- **`resource.h`** — Added `IDC_SNIP_SAVE_HOTKEY` control ID for the new
dialog control
- **`ZoomItSettings.h`** — Added `g_SnipSaveToggleKey` global variable
(default: `Ctrl+Shift+6`) and its `RegSettings[]` entry for registry
persistence
- **`ZoomIt.rc`** — Added a second hotkey control ("Snip Save Toggle")
to the standalone SNIP options dialog
- **`Zoomit.cpp`** — Added `g_SnipSaveToggleMod` global; replaced all 4
XOR-derived `RegisterHotKey` calls with the new explicit setting;
updated the options dialog init, read, validation, and save logic

### Settings Interop

- **`ZoomItSettings.cpp`** — Added `SnipSaveToggleKey` to the
`settings_with_special_semantics` map so it serializes as a hotkey JSON
object

### C# Settings UI

- **`ZoomItProperties.cs`** — Added `DefaultSnipSaveToggleKey` and
`SnipSaveToggleKey` property
- **`ZoomItViewModel.cs`** — Replaced the computed XOR-derived read-only
getter with a full get/set property backed by the new
`SnipSaveToggleKey` setting
- **`ZoomItPage.xaml`** — Replaced the read-only markdown description
with an editable `ShortcutControl` for the save hotkey
- **`Resources.resw`** — Added "Save snip to file" header string;
removed the old templated description


<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Built and tested locally --

 App shows additional shortcut for ZoomIt

<img width="983" height="188" alt="image"
src="https://github.com/user-attachments/assets/f73c1ecc-3aee-4dee-bfbb-b95764e7eb1c"
/>

 I am able to save files via `CTRL + S` as normal

 I am able to use Snip activation with `CTRL + Shift + S`

 I am able to use Save snip to file with the assigned shortcut of `CTRL
+ Shift + 6`

 Other ZoomIt commands run as expected (e.g. LiveZoom with my mapping
of `ALT + 4`

---------

Co-authored-by: Sean Killeen <SeanKilleen@gmail.com>
2026-07-02 01:49:21 +02:00
Dave Rayment
bf6ff579d3 [ZoomIt] Fix issue with recording filename suffixes (#43236)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fixes an issue where ZoomIt would always remove a numeric suffix from a
suggested recording filename even when it was part of a user-chosen
name.

Also: appends a timestamp instead of a numeric suffix for the default
filename, improving consistency with other tools and allowing for
correct name ordering in Explorer views.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
The `GetUniqueRecordingFilename()` function used regex to strip numeric
suffixes, and incorrectly assumed that all `(N)` patterns were
ZoomIt-generated. This broke user-provided filenames like "My
Presentation (2025).mp4".

### Root cause
```cpp
    // Chop off index if it's there
    auto base = std::regex_replace( path.stem().wstring(), std::wregex( L" [(][0-9]+[)]$" ), L"" );
    path.replace_filename( base + path.extension().wstring() );
```

This code strips off _any_ numeric suffix.

### Solution
The proposed solution tracks the user's chosen filename separately,
using it as the base for the file renaming strategy. The new string
`g_RecordingSaveBaseFilename` allows for additive suffix generation
without using regex stripping.

This change also allows us to remove **regex.h** as a dependency,
reducing the application's file size.

The code retains the addition of numeric suffixes for user-chosen
filenames, but timestamp suffixes are now added when the filename is the
default `Recording.mp4`; this is more consistent with tools like Windows
Snipping Tool, and allows for correct ordering of files in Windows
Explorer (previously, `Recording (11).mp4` would be sorted before
`Recording (2).mp4`, for example).

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

### Test Scenario 1: Default filename with timestamp suffix
1. Launch ZoomIt, start first recording with
<kbd>Ctrl</kbd>+<kbd>5</kbd>.
2. Stop the recording. The Save dialog shows "Recording.mp4".
3. Select **Save** to Accept this default. The file is saved as
"Recording 2025-11-03 180719.mp4" or similar.
4. Start and stop another recording.
5. The Save dialog shows "Recording 2025-11-03 180800.mp4" or similar,
i.e. with a distinct timestamp from the last save. Accept this
suggestion by selecting **Save**.
6. Verify both files exist on disk. By default, this will be in your
**Videos** folder.

### Test Scenario 2: Custom filename (ascending numeric suffix)
1. Launch ZoomIt, and start recording.
2. Stop the recording and change the suggested filename to "My
Presentation.mp4". Select **Save** to save the file.
3. Start and stop a second recording.
4. Confirm the dialog suggests "My Presentation (1).mp4" as the
filename. Select **Save** to accept this filename.
5. Start and stop a third recording.
6. Confirm the dialog suggests "My Presentation (2).mp4" as the
filename. Select **Save** to accept the suggestion.
7. Verify all files exist on disk with the correct names.

### Test Scenario 3: User filename with parenthetical number (bug repro)
1. Launch ZoomIt and start recording.
2. Stop the recording and change the suggested filename to "My
Presentation (2025).mp4". Select **Save** to save the recording to disk.
3. Start and stop a second recording.
4. Confirm the dialog suggests "My Presentation (2025) (1).mp4" as the
filename. (This was broken before - the prior version would suggest "My
Presentation.mp4".)
5. Accept the suggestion by selecting **Save**.
6. Start and stop a third recording.
7. Confirm the save dialog suggests "My Presentation (2025) (2).mp4".
8. Verify all files exist on disk with the correct names.

### Test Scenario 4: User modifies suggested filename
1. Save first recording as "Test.mp4".
2. Start and stop a second recording.
3. Confirm the second recording has a suggested filename of "Test
(1).mp4".
4. Change the suggested filename to "TestFinal.mp4" before saving.
5. Start and stop a third recording.
6. Confirm the save dialog suggests "TestFinal (1).mp4". (Verifies
correct updating of the base filename.)

### Test Scenario 5: Existing files at the save location
1. Manually create "Video.mp4" and "Video (1).mp4" in the Videos folder.
2. Start ZoomIt and save a recording to the same folder as "Video.mp4".
Confirm you are asked to overwrite the existing file.
3. Select "Yes" and save the file.
4. Start and stop another recording.
7. Confirm that the Save dialog's suggested filename is "Video (2).mp4",
skipping the manually-added file.

## Code updates

### Additional clean-up

- Clarified path construction in recording initialisation block
(replaced `/=` with explicit path building for a more readable
approach).
- Changed `DEFAULT_RECORDING_FILE` from a `#define` to a `constexpr`
instead. The new base filename variable is based upon it.

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-07-01 23:29:51 +02:00
Michael Jolley
0afe525f31 Fix memory leaks in Command Palette: unsubscribe event handlers and dispose resources (#48884)
## Summary

Fixes 6 memory leaks in Command Palette caused by event handlers not
being unsubscribed and disposable resources not being released.

## Changes

| File | Fix |
|------|-----|
| `MainListPage.cs` | Replace lambda on static
`AllAppsCommandProvider.Page.PropChanged` with named method; unsubscribe
in `Dispose()` |
| `WinRTExtensionService.cs` | Unsubscribe static `PackageCatalog`
events (`PackageInstalling`/`Uninstalling`/`Updating`) in `Dispose()` |
| `MainWindow.xaml.cs` | Unsubscribe all event handlers in `Dispose()`
(`SizeChanged`, `SettingsChanged`, `ActualThemeChanged`,
`XamlRoot.Changed`, `CardElement.SizeChanged`, timer `Tick`,
`ThemeChanged`, `KeyPressed`); replace lambda with named method |
| `ContentFormControl.xaml.cs` | Unsubscribe previous
`RenderedAdaptiveCard.Action` before subscribing to new card |
| `BlurImageControl.cs` | Track `LoadedImageSurface` and unsubscribe
`LoadCompleted` before loading a new image |
| `ShowFileInFolderCommand.cs` | Dispose `Process` object returned by
`Process.Start()` (handle leak) |

## Validation

- [x] Build clean (`tools\build\build.ps1 -Path src\modules\cmdpal
-Platform x64 -Configuration Debug`) — exit code 0
- [x] All 1809 CmdPal unit tests pass (2 pre-existing skips)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 18:59:55 +02:00
Michael Jolley
a43fb12d6f cmdpal: Support Enter key to submit FormContent (Adaptive Card) inputs (#48768)
## Summary

Adds Enter key support for submitting Adaptive Card forms in the Command
Palette. When a user presses Enter inside a single-line `Input.Text`
field, the form is automatically submitted using the first
`Action.Submit` or `Action.Execute` action on the card.

## Problem

When extensions use `FormContent` with Adaptive Cards, pressing Enter
inside an `Input.Text` field does not trigger submission. Users must
click the submit button with their mouse (or tab to it), breaking
keyboard-only workflows like login/unlock forms.

## Solution

Added a `KeyDown` event handler on the rendered Adaptive Card's
`FrameworkElement` in `ContentFormControl.xaml.cs`:

- Intercepts `VirtualKey.Enter` when the source is a `TextBox` that
doesn't accept returns (single-line)
- Finds the first `AdaptiveSubmitAction` or `AdaptiveExecuteAction` on
the card
- Calls `HandleSubmit` with that action and the current user inputs via
`RenderedAdaptiveCard.UserInputs.AsJson()`

Multiline text inputs (`AcceptsReturn = true`) are excluded so Enter
still inserts newlines.

## Validation

- Single file change in `ContentFormControl.xaml.cs`
- Uses existing `HandleSubmit` path — same behavior as clicking the
submit button
- No impact on cards without submit/execute actions
- No impact on multiline text inputs

Fixes #46003

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 10:48:32 -05:00
Christian Gaarden Gaardmark
bc56443443 New++ updated attribution (#49047)
## Summary of the Pull Request
* Updated New+ attribution text and link after conversation with Niels
* Text="Based on Christian Gaardmark's New++ from the Productivity Plus
Pack"
*
Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt"

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

## Detailed Description of the Pull Request / Additional comments
* Text="Based on Christian Gaardmark's New++ from the Productivity Plus
Pack"
*
Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt"

## Validation Steps Performed
1. Manually confirmed visual layout
2. Manually confirmed link

<img width="470" height="65" alt="image"
src="https://github.com/user-attachments/assets/56d6e454-89cf-4a26-b570-b162df51f4a9"
/>

cc:
@niels9001
2026-07-01 14:18:05 +02:00
Clint Rutkas
3298625b67 Cancel stale Quick Accent toolbar render timer (#48944)
## Summary

Quick Accent's ShowToolbar queues a delayed render of the accent menu
through Task.Delay(...).ContinueWith(...). The continuation only checked
_visible before rendering, so a timer started by an earlier key press
could still fire for a **newer** summon — or one that had already been
hidden — popping the accent menu earlier than the configured delay
intended.

This was surfaced while reviewing the press-and-hold work in #48937, but
it is a pre-existing race independent of that feature, so it ships on
its own.

## Fix

- Tag each `ShowToolbar` summon with an incrementing `_showGeneration`
id and capture it in the local closure.
- The delayed continuation now renders only when it is still the most
recent summon (`generation == _showGeneration`) **and** `_visible`.
- Bump the generation when the toolbar hides, so any in-flight timer
queued before the hide is cancelled.

Everything runs on the UI thread (the dispatcher marshals
`ShowToolbar`/hide and the continuation uses
`FromCurrentSynchronizationContext`), so the counter needs no locking.

## Validation

- Built `PowerAccent.UI` (C++ hook + Core) Debug|x64 — 0 warnings, 0
errors.
- Manual: rapid repeated taps of an accent-capable letter no longer
flash the menu early from a leftover timer; normal hold-to-open and
navigation/commit are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 15:36:50 +08:00
Clint Rutkas
ae9f241ef1 [FancyZones] Harden three shutdown races in WorkArea / ZonesOverlay / OnThreadExecutor (#48473)
## Summary

Four small shutdown-/teardown-race fixes in FancyZones that I spotted
while reading through the work-area and overlay teardown sequence for an
unrelated review. Each one is independently safe in the happy path, but
in combination they can crash the FancyZones host process during display
changes, monitor configuration changes, a settings toggle mid-drag, or
normal exit.

## Issues fixed

### 1. `~ZonesOverlay` joins a non-joinable thread when the constructor
early-returns
`ZonesOverlay::ZonesOverlay` can return early in two places — if
`GetClientRect` fails or if `CreateHwndRenderTarget` returns a failure
HRESULT (both reachable in the wild during a display-driver TDR or when
a monitor is disconnected mid-init). When that happens, `m_renderThread`
is never started and stays default-constructed. The destructor
unconditionally calls `m_renderThread.join()`, which on a non-joinable
thread is undefined behavior (MSVC throws `std::system_error`); thrown
from an implicit-noexcept destructor it calls `std::terminate()`.

Fix: guard the wake-up-and-join sequence with `if
(m_renderThread.joinable())`.

### 2. `~WorkArea` returns the HWND to the window pool before the
renderer is torn down
`WorkArea`'s explicit destructor body calls
`windowPool.FreeZonesOverlayWindow(m_window)` first, and only afterwards
does implicit member destruction run `~ZonesOverlay` (which joins the
render thread). Between those two steps the HWND is back in the pool and
immediately eligible for reuse by the next `NewZonesOverlayWindow` call,
while the still-alive render thread is using `m_renderTarget` to draw
into it. If the pool hands the same HWND to a freshly-built
`ZonesOverlay`, two render targets target the same window concurrently.

Fix: reset `m_zonesOverlay` (which joins the render thread) before
returning the window to the pool.

### 3. `~OnThreadExecutor` writes `_shutdown_request` outside the mutex
The destructor mutates the shutdown flag without holding `_task_mutex`,
then calls `_task_cv.notify_one()`. The worker checks the same flag
inside `_task_cv.wait(lock, predicate)`. The atomic does make the value
visible eventually, but if the notify lands in the narrow window where
the worker has just evaluated the predicate as false and is about to
atomically release the lock and sleep, the wakeup can be missed and
`_worker_thread.join()` hangs.

Fix: take `_task_mutex` around the `_shutdown_request = true` write so
it pairs correctly with the `cv.wait`.

### 4. `WindowMouseSnap` keeps a dangling `WorkArea*` across
`WorkAreaConfiguration::Clear()`
`FancyZones::UpdateWorkAreas()` rebuilds `m_workAreaConfiguration`
whenever monitor state changes mid-session, and the
`SpanZonesAcrossMonitors` settings toggle hits the same `Clear()`. If
the user is mid-drag at the moment one of these runs, the
`WindowMouseSnap` instance owned by `FancyZones` is still holding both a
`const` reference to the map being cleared (`m_activeWorkAreas`) and a
raw `WorkArea*` into one of the entries that's about to be destroyed
(`m_currentWorkArea`). The next `WM_MOUSEMOVE` -> `MoveSizeUpdate()`
then dereferences a freed pointer. `WindowMouseSnap`'s destructor only
resets window transparency, so relying on it doesn't help; the snapper
has to be torn down explicitly.

Fix: call `FancyZones::MoveSizeEnd()` (which already tears down the
snapper cleanly and is a no-op when the snapper is null) before each
`m_workAreaConfiguration.Clear()` call on these paths.

## Risk

Low. All four changes are localized to teardown / reconfiguration paths
and only tighten existing destruction sequences — the steady-state
behavior of `ZonesOverlay::Render`/`Show`/`Hide`, the work-area public
API, `OnThreadExecutor::submit`/`cancel`, and `WindowMouseSnap` drag
handling is unchanged. The `WorkArea` reordering is the most behavioral
change; it now guarantees the render thread has stopped using the HWND
before the pool can recycle it, which is what the existing
implicit-member-destruction order already implied but couldn't enforce
given the explicit destructor body.

## Validation

Spot-built locally; this repo's `dotnet restore` runtime-pack issue
(unrelated to this PR — same NU1102 pattern that's affecting other open
PRs) prevents a full `Build.cmd` here, but the C++ FancyZones modules
involved are unchanged in their public surface and are exercised by
existing unit tests in `FancyZonesTests` for the WorkArea code paths.

---

ADO: https://microsoft.visualstudio.com/OS/_workitems/edit/54653316/

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 15:35:03 +08:00
Jiří Polášek
67a9fa2d13 CmdPal: Ensure that directory paths passed from Bookmarks is quoted (#48955)
This PR ensures that directory paths passed from Bookmarks through
`CommandLauncher` to Windows Explorer are properly quoted. In current
implementation the directory path that should be opened through Windows
Explorer is not quoted and if it contains a space then Explorer will
treat it as multiple arguments instead.

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-06-30 23:24:01 -05:00
Clint Rutkas
1cfc923bdb Fix mismatched WebView2 versions, upgrade WebView2 (#49051) 2026-06-30 18:18:06 -05:00
Clint Rutkas
2dd802f367 Fixing Windows.ImplementationLibrary mismatch between proj and package.config (#49050)
In the vcxproj files, it lists it correctly for
Microsoft.Windows.ImplementationLibrary.1.0.260126.7 but the package
files are incorrect
2026-06-30 18:17:27 -05:00
Clint Rutkas
a0d17406ba Updating MessagePack (#49029)
Version bump on Message Pack
2026-06-30 14:26:32 +02:00
Copilot
4a27c5d5f9 New+: Fix French translation guidance (Nouveau+ not Nouveauté+) (#47225)
## Summary of the Pull Request

French translation of "New+" was rendered as "Nouveauté+" ("Novelty+")
instead of "Nouveau+" ("New+"), inconsistent with how Windows itself
translates the "New" context menu item in French. This updates
translator guidance comments in the English resource files to explicitly
call out the correct French form.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

Translator-facing `<comment>` fields updated across three resource files
to explicitly state the correct French translation and flag the wrong
one:

- **`NewShellExtensionContextMenu/resources.resx`** and
**`NewShellExtensionContextMenu.win10/resources.resx`** —
`context_menu_item_new`:
> _"…e.g. Danish it would become Ny+, **French it would become Nouveau+
(not Nouveauté+)**"_

- **`Settings.UI/Strings/en-us/Resources.resw`** — five `NewPlus.*` /
`Oobe_NewPlus.*` strings:
> _"…Localize product name in accordance with Windows New. **e.g. French
would be Nouveau+ (not Nouveauté+)**"_

Actual `.lcl` translation files are managed by the CDPX localization
pipeline; these comment updates feed directly into the guidance the
localization team sees when updating those files.

## Validation Steps Performed

Comment-only changes to XML resource files; no runtime behavior
affected. Verified all targeted entries were updated and no existing
checked-in `Nouveauté` strings remain in the repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-06-30 17:06:25 +08:00
57 changed files with 795 additions and 263 deletions

View File

@@ -1234,6 +1234,8 @@ NOTSRCCOPY
NOTSRCERASE
Notupdated
notwindows
NOTXORPEN
Nouveaut
nowarn
NOZORDER
NPH

View File

@@ -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
---

View File

@@ -26,7 +26,7 @@
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
@@ -38,7 +38,7 @@
<PackageVersion Include="Mages" Version="3.0.0" />
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
@@ -64,7 +64,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.71.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.71.0-alpha" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.8" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
@@ -76,7 +76,7 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
@@ -151,4 +151,4 @@
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
</packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
</packages>

View File

@@ -39,6 +39,7 @@
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natstepfilter" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
</packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
</packages>

View File

@@ -119,7 +119,7 @@
</resheader>
<data name="context_menu_item_new" xml:space="preserve">
<value>New+</value>
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="context_menu_item_open_templates" xml:space="preserve">
<value>Open templates</value>

View File

@@ -119,7 +119,7 @@
</resheader>
<data name="context_menu_item_new" xml:space="preserve">
<value>New+</value>
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="context_menu_item_open_templates" xml:space="preserve">
<value>Open templates</value>

View File

@@ -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

View File

@@ -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) },

View File

@@ -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))

View File

@@ -93,7 +93,6 @@
#include <algorithm>
#include <filesystem>
#include <future>
#include <regex>
#include <fstream>
#include <sstream>

View File

@@ -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

View File

@@ -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 },

View File

@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
namespace Microsoft.CmdPal.Common.Helpers;
public static class ShellArgumentBuilder
{
public static string BuildArguments(params string[] arguments)
{
if (arguments.Length <= 0)
{
return string.Empty;
}
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
private static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(' ');
}
if (argument.Length == 0 || ShouldBeQuoted(argument))
{
stringBuilder.Append('"');
var index = 0;
while (index < argument.Length)
{
var c = argument[index++];
if (c == '\\')
{
var numBackSlash = 1;
while (index < argument.Length && argument[index] == '\\')
{
index++;
numBackSlash++;
}
if (index == argument.Length)
{
stringBuilder.Append('\\', numBackSlash * 2);
}
else if (argument[index] == '"')
{
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
stringBuilder.Append('"');
index++;
}
else
{
stringBuilder.Append('\\', numBackSlash);
}
continue;
}
if (c == '"')
{
stringBuilder.Append('\\');
stringBuilder.Append('"');
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append('"');
}
else
{
stringBuilder.Append(argument);
}
}
private static bool ShouldBeQuoted(string argument)
{
foreach (var c in argument)
{
if (char.IsWhiteSpace(c) || c == '"')
{
return true;
}
}
return false;
}
}

View File

@@ -94,6 +94,7 @@ internal sealed partial class HttpCachingClient : IDisposable
public void Dispose()
{
_httpClient.Dispose();
_cacheHandler.Dispose();
}
private static bool IsSupportedHttpUri(Uri resourceUri)

View File

@@ -146,13 +146,7 @@ public sealed partial class MainListPage : DynamicListPage,
// The all apps page will kick off a BG thread to start loading apps.
// We just want to know when it is done.
var allApps = AllAppsCommandProvider.Page;
allApps.PropChanged += (s, p) =>
{
if (p.PropertyName == nameof(allApps.IsLoading))
{
IsLoading = ActuallyLoading();
}
};
allApps.PropChanged += AllApps_PropChanged;
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
@@ -172,6 +166,14 @@ public sealed partial class MainListPage : DynamicListPage,
}
}
private void AllApps_PropChanged(object? sender, IPropChangedEventArgs e)
{
if (e.PropertyName == nameof(AllAppsCommandProvider.Page.IsLoading))
{
IsLoading = ActuallyLoading();
}
}
private void PinnedCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
_defaultViewDirty = true;
@@ -782,6 +784,8 @@ public sealed partial class MainListPage : DynamicListPage,
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
_tlcManager.PinnedCommands.CollectionChanged -= PinnedCommands_CollectionChanged;
AllAppsCommandProvider.Page.PropChanged -= AllApps_PropChanged;
if (_settingsService is not null)
{
_settingsService.SettingsChanged -= SettingsChangedHandler;

View File

@@ -541,6 +541,9 @@ public partial class WinRTExtensionService : IExtensionService, IDisposable
{
if (disposing)
{
_catalog.PackageInstalling -= Catalog_PackageInstalling;
_catalog.PackageUninstalling -= Catalog_PackageUninstalling;
_catalog.PackageUpdating -= Catalog_PackageUpdating;
_getInstalledExtensionsLock.Dispose();
}

View File

@@ -94,6 +94,7 @@ internal sealed partial class BlurImageControl : Control
private SpriteVisual? _effectVisual;
private CompositionEffectBrush? _effectBrush;
private CompositionSurfaceBrush? _imageBrush;
private LoadedImageSurface? _lastLoadedSurface;
public BlurImageControl()
{
@@ -379,10 +380,20 @@ internal sealed partial class BlurImageControl : Control
}
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
// Each call to LoadImageAsync creates a new LoadedImageSurface backed by native
// composition resources. The old surface becomes unrooted once the brush points at
// the new one, so it isn't leaked, but dispose it explicitly so the unmanaged
// resources are released deterministically instead of waiting for finalization.
var previousSurface = _lastLoadedSurface;
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
_lastLoadedSurface = loadedSurface;
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
SetLoadedSurfaceToBrush(loadedSurface);
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
previousSurface?.Dispose();
}
catch (Exception ex)
{

View File

@@ -8,7 +8,9 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.System;
namespace Microsoft.CmdPal.UI.Controls;
@@ -22,6 +24,7 @@ public sealed partial class ContentFormControl : UserControl
// tree. If this gets GC'ed, then it'll revoke our Action handler, and the
// form will do seemingly nothing.
private RenderedAdaptiveCard? _renderedCard;
private AdaptiveCard? _adaptiveCard;
public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); }
@@ -95,9 +98,11 @@ public sealed partial class ContentFormControl : UserControl
private void DisplayCard(AdaptiveCardParseResult result)
{
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
_adaptiveCard = result.AdaptiveCard;
ContentGrid.Children.Clear();
if (_renderedCard.FrameworkElement is not null)
{
_renderedCard.FrameworkElement.KeyDown += OnFormKeyDown;
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
// Use the Loaded event to ensure we focus after the card is in the visual tree
@@ -114,8 +119,9 @@ public sealed partial class ContentFormControl : UserControl
private void OnFrameworkElementLayoutUpdated(object? sender, object e)
{
// Only fix once — unhook after first layout pass
if (_renderedCard?.FrameworkElement is FrameworkElement element)
// Only fix once — unhook from sender (not _renderedCard, which may have been
// reassigned by the time this fires).
if (sender is FrameworkElement element)
{
element.LayoutUpdated -= OnFrameworkElementLayoutUpdated;
FixToggleAccessibilityNames(element);
@@ -276,6 +282,50 @@ public sealed partial class ContentFormControl : UserControl
return null;
}
private void OnFormKeyDown(object sender, KeyRoutedEventArgs e)
{
// Snapshot the fields so a subsequent DisplayCard call can't swap the
// rendered/parsed card out from under us mid-method. This keeps the
// resolved submit action and the gathered inputs from the same card.
var renderedCard = _renderedCard;
var adaptiveCard = _adaptiveCard;
if (e.Key != VirtualKey.Enter || renderedCard == null || adaptiveCard == null)
{
return;
}
// Only submit when Enter is pressed inside a single-line TextBox
if (e.OriginalSource is TextBox textBox && !textBox.AcceptsReturn)
{
// Find the first Submit or Execute action on the card
IAdaptiveActionElement? submitAction = null;
foreach (var action in adaptiveCard.Actions)
{
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
{
submitAction = action;
break;
}
}
if (submitAction != null)
{
e.Handled = true;
// Validate (and gather) the inputs before submitting. AsJson() only
// returns the values cached by a successful ValidateInputs() call, so
// skipping this would submit an empty payload. This mirrors what the
// renderer does internally when a submit button is clicked.
var inputs = renderedCard.UserInputs;
if (inputs.ValidateInputs(submitAction))
{
ViewModel?.HandleSubmit(submitAction, inputs.AsJson());
}
}
}
}
private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) =>
ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson());
}

View File

@@ -192,7 +192,7 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
RootElement.ActualThemeChanged += RootElement_ActualThemeChanged;
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
@@ -222,6 +222,11 @@ public sealed partial class MainWindow : WindowEx,
UpdateBackdrop();
}
private void RootElement_ActualThemeChanged(FrameworkElement sender, object args)
{
DispatcherQueue.TryEnqueue(UpdateBackdrop);
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
@@ -1683,6 +1688,9 @@ public sealed partial class MainWindow : WindowEx,
public void Dispose()
{
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged -= SettingsChangedHandler;
_localKeyboardListener.Dispose();
_windowThemeSynchronizer.Dispose();
DisposeAcrylic();

View File

@@ -22,6 +22,7 @@ public sealed partial class ExtensionsPage : Page
private readonly SettingsViewModel? viewModel;
private readonly Dictionary<string, WeakReference<SettingsCard>> _vmToCardMap = new();
private readonly Dictionary<SettingsCard, ProviderSettingsViewModel> _cardToVmMap = new();
public ExtensionsPage()
{
@@ -31,6 +32,23 @@ public sealed partial class ExtensionsPage : Page
var themeService = App.Current.Services.GetService<IThemeService>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
Unloaded += ExtensionsPage_Unloaded;
}
private void ExtensionsPage_Unloaded(object sender, RoutedEventArgs e)
{
// ProviderSettingsViewModel subscribes to its CommandProviderWrapper (owned by the
// singleton TopLevelCommandManager), so a live VM roots this page through the
// PropertyChanged handler below. Drain any VMs still hooked when the page is torn
// down; SettingsCard_DataContextChanged only unhooks the ones that get recycled.
foreach (var vm in _cardToVmMap.Values)
{
vm.PropertyChanged -= ProviderViewModel_PropertyChanged;
}
_cardToVmMap.Clear();
_vmToCardMap.Clear();
}
private void SettingsCard_Click(object sender, RoutedEventArgs e)
@@ -46,16 +64,28 @@ public sealed partial class ExtensionsPage : Page
private void SettingsCard_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
if (sender is SettingsCard card && card.DataContext is ProviderSettingsViewModel newVm)
if (sender is SettingsCard card)
{
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
// Immediately update automation name in case DisplayName is already available
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
// Unsubscribe from the previous ViewModel to prevent handler accumulation
// when virtualization recycles items with a new DataContext.
if (_cardToVmMap.TryGetValue(card, out var oldVm))
{
AutomationProperties.SetName(toggle, newVm.DisplayName);
oldVm.PropertyChanged -= ProviderViewModel_PropertyChanged;
_cardToVmMap.Remove(card);
}
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
if (card.DataContext is ProviderSettingsViewModel newVm)
{
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
_cardToVmMap[card] = newVm;
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
// Immediately update automation name in case DisplayName is already available
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
{
AutomationProperties.SetName(toggle, newVm.DisplayName);
}
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Helpers;
namespace Microsoft.CmdPal.Common.UnitTests.Helpers;
[TestClass]
public class ShellArgumentBuilderTests
{
[DataTestMethod]
[DataRow("plain", "plain")]
[DataRow("C:\\Program Files\\PowerToys", "\"C:\\Program Files\\PowerToys\"")]
[DataRow("say \"hello\"", "\"say \\\"hello\\\"\"")]
[DataRow("", "\"\"")]
[DataRow("C:\\Program Files\\", "\"C:\\Program Files\\\\\"")]
public void BuildArguments_FormatsSingleArgument(string argument, string expected)
{
var actual = ShellArgumentBuilder.BuildArguments(argument);
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void BuildArguments_FormatsMultipleArguments()
{
var actual = ShellArgumentBuilder.BuildArguments("plain", "C:\\Program Files\\PowerToys", "two words");
Assert.AreEqual("plain \"C:\\Program Files\\PowerToys\" \"two words\"", actual);
}
}

View File

@@ -5,6 +5,7 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
@@ -24,7 +25,7 @@ internal static class CommandLauncher
// You can notice the difference with Recycle Bin for example:
// - "explorer ::{645FF040-5081-101B-9F08-00AA002F954E}"
// - "::{645FF040-5081-101B-9F08-00AA002F954E}"
return ShellHelpers.OpenInShell("explorer.exe", classification.Target);
return ShellHelpers.OpenInShell("explorer.exe", ShellArgumentBuilder.BuildArguments(classification.Target));
case LaunchMethod.ActivateAppId:
return ActivateAppId(classification.Target, classification.Arguments);

View File

@@ -11,6 +11,7 @@
<ProjectPriFileName>Microsoft.CmdPal.Ext.Bookmarks.pri</ProjectPriFileName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
</ItemGroup>

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
@@ -42,98 +42,7 @@ public class ShellListPageHelpers
executable = segments[0];
if (segments.Length > 1)
{
arguments = ArgumentBuilder.BuildArguments(segments[1..]);
}
}
private static class ArgumentBuilder
{
internal static string BuildArguments(string[] arguments)
{
if (arguments.Length <= 0)
{
return string.Empty;
}
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
private static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(' ');
}
if (argument.Length == 0 || ShouldBeQuoted(argument))
{
stringBuilder.Append('\"');
var index = 0;
while (index < argument.Length)
{
var c = argument[index++];
if (c == '\\')
{
var numBackSlash = 1;
while (index < argument.Length && argument[index] == '\\')
{
index++;
numBackSlash++;
}
if (index == argument.Length)
{
stringBuilder.Append('\\', numBackSlash * 2);
}
else if (argument[index] == '\"')
{
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
stringBuilder.Append('\"');
index++;
}
else
{
stringBuilder.Append('\\', numBackSlash);
}
continue;
}
if (c == '\"')
{
stringBuilder.Append('\\');
stringBuilder.Append('\"');
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append('\"');
}
else
{
stringBuilder.Append(argument);
}
}
private static bool ShouldBeQuoted(string s)
{
foreach (var c in s)
{
if (char.IsWhiteSpace(c) || c == '\"')
{
return true;
}
}
return false;
arguments = ShellArgumentBuilder.BuildArguments(segments[1..]);
}
}
}

View File

@@ -27,7 +27,7 @@ public partial class ShowFileInFolderCommand : InvokableCommand
try
{
var argument = "/select, \"" + _path + "\"";
Process.Start("explorer.exe", argument);
using var process = Process.Start("explorer.exe", argument);
}
catch (Exception)
{

View File

@@ -866,6 +866,14 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept
std::vector<FancyZonesDataTypes::MonitorId> monitors = { FancyZonesDataTypes::MonitorId{ .monitor = nullptr, .deviceId = { .id = ZonedWindowProperties::MultiMonitorName, .instanceId = ZonedWindowProperties::MultiMonitorInstance } } };
if (ShouldWorkAreasBeRecreated(monitors, currentVirtualDesktop, m_workAreaConfiguration.GetAllWorkAreas()))
{
// WindowMouseSnap caches a raw WorkArea* in m_currentWorkArea and the
// WorkArea map by reference. WorkAreaConfiguration::Clear() destroys
// every unique_ptr<WorkArea> (and hence the inner ZonesOverlay and
// its std::mutex). If a drag is in flight, the next MoveSizeUpdate
// would dereference that dangling WorkArea* and lock the freed
// mutex. Drain the active drag first so subsequent drag messages
// hit the snapper's `if (m_windowMouseSnapper)` guard and no-op.
MoveSizeEnd();
m_workAreaConfiguration.Clear();
FancyZonesDataTypes::WorkAreaId workAreaId;
@@ -882,6 +890,8 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept
if (ShouldWorkAreasBeRecreated(monitors, currentVirtualDesktop, workAreas))
{
// See comment above the matching Clear() in the span-zones branch.
MoveSizeEnd();
m_workAreaConfiguration.Clear();
for (const auto& monitor : monitors)
{
@@ -1094,6 +1104,9 @@ void FancyZones::SettingsUpdate(SettingId id)
break;
case SettingId::SpanZonesAcrossMonitors:
{
// See UpdateWorkAreas() — same WindowMouseSnap dangling-WorkArea*
// hazard if the user toggles this setting mid-drag.
MoveSizeEnd();
m_workAreaConfiguration.Clear();
PostMessageW(m_window, WM_PRIV_INIT, NULL, NULL);
}

View File

@@ -48,7 +48,14 @@ void OnThreadExecutor::worker_thread()
OnThreadExecutor::~OnThreadExecutor()
{
_shutdown_request = true;
{
// Modify the shared shutdown flag while holding the mutex so the
// worker reliably observes it on its next wake. Without this, a notify
// racing the worker entering _task_cv.wait can be missed and the join
// below hangs forever.
std::lock_guard lock{ _task_mutex };
_shutdown_request = true;
}
_task_cv.notify_one();
_worker_thread.join();
}

View File

@@ -115,6 +115,11 @@ WorkArea::WorkArea(HINSTANCE hinstance, const FancyZonesDataTypes::WorkAreaId& u
WorkArea::~WorkArea()
{
// Tear down the renderer (joining its background thread) before returning
// the HWND to the pool. Otherwise, the render thread can still be drawing
// through m_renderTarget into an HWND that has already been recycled by a
// subsequent NewZonesOverlayWindow call.
m_zonesOverlay.reset();
windowPool.FreeZonesOverlayWindow(m_window);
}

View File

@@ -340,13 +340,19 @@ void ZonesOverlay::DrawActiveZoneSet(const ZonesMap& zones,
ZonesOverlay::~ZonesOverlay()
{
// Constructor early-returns (e.g. CreateHwndRenderTarget failing during a
// display-driver TDR) leave m_renderThread default-constructed; calling
// join() on a non-joinable thread terminates the process.
if (m_renderThread.joinable())
{
std::unique_lock lock(m_mutex);
m_abortThread = true;
m_shouldRender = true;
{
std::unique_lock lock(m_mutex);
m_abortThread = true;
m_shouldRender = true;
}
m_cv.notify_all();
m_renderThread.join();
}
m_cv.notify_all();
m_renderThread.join();
if (m_renderTarget)
{

View File

@@ -59,7 +59,6 @@
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
<PropertyGroup Label="Configuration">
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
<CharacterSet>Unicode</CharacterSet>
<SpectreMitigation>Spectre</SpectreMitigation>
@@ -164,7 +163,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets" Condition="Exists('..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" />
</ImportGroup>
<Import Project="..\..\..\..\deps\spdlog.props" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
@@ -181,7 +180,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
<Target Name="FakeResourcesPriMerge" BeforeTargets="FinalizeBuildStatus" DependsOnTargets="CopyFilesToOutputDirectory">
<Message Text="Renaming Microsoft.UI.Xaml.pri to resources.pri" />

View File

@@ -3,6 +3,6 @@
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
<package id="Microsoft.UI.Xaml" version="2.8.2-prerelease.220830001" targetFramework="native" />
<package id="Microsoft.VCRTForwarders.140" version="1.0.7" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.4022.49" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
</packages>

View File

@@ -15,7 +15,6 @@
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Label="Configuration">
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
@@ -101,7 +100,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" />
</ImportGroup>
<Import Project="..\..\..\..\deps\spdlog.props" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
@@ -114,7 +113,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />

View File

@@ -2,6 +2,6 @@
<packages>
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
<package id="Microsoft.UI.Xaml" version="2.8.2-prerelease.220830001" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.4022.49" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
</packages>

View File

@@ -326,6 +326,7 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
std::wstring origKeysStr = origShortcut.ToHstringVK().c_str();
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
mapping->targetApp = AllocateAndCopyString(app);
mapping->exactMatch = origShortcut.exactMatch ? 1 : 0;
if (targetShortcutUnion.index() == 0)
{
@@ -429,6 +430,7 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
mapping->targetApp = AllocateAndCopyString(app);
mapping->exactMatch = origShortcut.exactMatch ? 1 : 0;
if (targetShortcutUnion.index() == 0)
{
@@ -533,11 +535,13 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
const wchar_t* startDirectory,
int elevation,
int ifRunningAction,
int visibility)
int visibility,
int exactMatch)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
Shortcut originalShortcut(originalKeys);
originalShortcut.exactMatch = (exactMatch != 0);
KeyShortcutTextUnion targetShortcut;

View File

@@ -33,6 +33,7 @@ struct ShortcutMapping
wchar_t* programPath;
wchar_t* programArgs;
wchar_t* uriToOpen;
int exactMatch;
};
extern "C"
@@ -69,7 +70,8 @@ extern "C"
const wchar_t* startDirectory = nullptr,
int elevation = 0,
int ifRunningAction = 0,
int visibility = 0);
int visibility = 0,
int exactMatch = 0);
__declspec(dllexport) void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount);
__declspec(dllexport) int GetKeyCodeFromName(const wchar_t* keyName);

View File

@@ -111,6 +111,10 @@
x:Uid="AllowChordsCheckBox"
Click="AllowChordsCheckBox_Click"
IsChecked="True" />
<CheckBox
x:Name="ExactMatchCheckBox"
x:Uid="ExactMatchCheckBox"
IsEnabled="False" />
</StackPanel>
</tkcontrols:Case>
<!-- Mouse Button Trigger -->

View File

@@ -154,6 +154,7 @@ namespace KeyboardManagerEditorUI.Controls
_triggerKeys.CollectionChanged += (_, _) =>
{
UpdatePlaceholderVisibility();
UpdateAppSpecificCheckBoxState();
RaiseValidationStateChanged();
};
@@ -561,6 +562,16 @@ namespace KeyboardManagerEditorUI.Controls
AppSpecificCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
// Exact match only applies to shortcuts (multiple keys), matching the classic editor.
if (ExactMatchCheckBox != null)
{
ExactMatchCheckBox.IsEnabled = isShortcut;
if (!isShortcut)
{
ExactMatchCheckBox.IsChecked = false;
}
}
}
finally
{
@@ -860,6 +871,23 @@ namespace KeyboardManagerEditorUI.Controls
}
}
/// <summary>
/// Gets a value indicating whether the origin shortcut requires an exact match
/// (i.e., no extra modifiers may be pressed). Only meaningful for shortcuts.
/// </summary>
public bool GetExactMatch() => ExactMatchCheckBox?.IsChecked == true && ExactMatchCheckBox?.IsEnabled == true;
/// <summary>
/// Sets the exact-match state for the origin shortcut.
/// </summary>
public void SetExactMatch(bool value)
{
if (ExactMatchCheckBox != null)
{
ExactMatchCheckBox.IsChecked = value;
}
}
/// <summary>
/// Sets the action type.
/// </summary>
@@ -1170,6 +1198,12 @@ namespace KeyboardManagerEditorUI.Controls
AppSpecificCheckBox.IsEnabled = false;
}
if (ExactMatchCheckBox != null)
{
ExactMatchCheckBox.IsChecked = false;
ExactMatchCheckBox.IsEnabled = false;
}
// Reset app combo boxes
if (ElevationComboBox != null)
{

View File

@@ -34,5 +34,7 @@ namespace KeyboardManagerEditorUI.Helpers
public string IfRunningAction { get; set; } = string.Empty;
public string Visibility { get; set; } = string.Empty;
public bool ExactMatch { get; set; }
}
}

View File

@@ -22,6 +22,8 @@ namespace KeyboardManagerEditorUI.Helpers
public string AppName { get; set; } = string.Empty;
public bool ExactMatch { get; set; }
private bool IsEnabledValue { get; set; } = true;
public string Id { get; set; } = string.Empty;

View File

@@ -17,7 +17,7 @@ namespace KeyboardManagerEditorUI.Helpers
{
public static class RemappingHelper
{
public static bool SaveMapping(KeyboardMappingService mappingService, List<string> originalKeys, List<string> remappedKeys, bool isAppSpecific, string appName, bool saveToSettings = true)
public static bool SaveMapping(KeyboardMappingService mappingService, List<string> originalKeys, List<string> remappedKeys, bool isAppSpecific, string appName, bool exactMatch = false, bool saveToSettings = true)
{
if (mappingService == null)
{
@@ -76,15 +76,16 @@ namespace KeyboardManagerEditorUI.Helpers
OriginalKeys = originalKeysString,
TargetKeys = targetKeysString,
TargetApp = isAppSpecific ? appName : string.Empty,
ExactMatch = exactMatch,
};
if (isAppSpecific && !string.IsNullOrEmpty(appName))
{
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName, exactMatch: exactMatch);
}
else
{
mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, exactMatch: exactMatch);
}
if (saveToSettings)

View File

@@ -20,6 +20,8 @@ namespace KeyboardManagerEditorUI.Helpers
public string AppName { get; set; } = string.Empty;
public bool ExactMatch { get; set; }
public bool IsActive { get; set; } = true;
public string Id { get; set; } = string.Empty;

View File

@@ -23,5 +23,7 @@ namespace KeyboardManagerEditorUI.Helpers
public bool IsAllApps { get; set; } = true;
public string AppName { get; set; } = string.Empty;
public bool ExactMatch { get; set; }
}
}

View File

@@ -248,9 +248,15 @@ namespace KeyboardManagerEditorUI.Helpers
}
}
// Check shortcut mappings
// Check shortcut mappings. Only key-output remaps (RemapShortcut) can make a key
// reachable again; program/URL/text targets do not restore keyboard input.
foreach (var mapping in mappingService.GetShortcutMappings())
{
if (mapping.OperationType != ShortcutOperationType.RemapShortcut)
{
continue;
}
string[] targetKeys = mapping.TargetKeys.Split(';');
if (targetKeys.Length == 1 && int.TryParse(targetKeys[0], out int shortcutTargetKey) && shortcutTargetKey == originalKey)
{

View File

@@ -83,7 +83,8 @@ namespace KeyboardManagerEditorUI.Interop
[MarshalAs(UnmanagedType.LPWStr)] string? startDirectory = null,
int elevation = 0,
int ifRunningAction = 0,
int visibility = 0);
int visibility = 0,
int exactMatch = 0);
// Delete Mapping Functions
[DllImport(DllName, CallingConvention = Convention)]
@@ -165,6 +166,7 @@ namespace KeyboardManagerEditorUI.Interop
public IntPtr ProgramPath;
public IntPtr ProgramArgs;
public IntPtr UriToOpen;
public int ExactMatch;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

View File

@@ -71,6 +71,7 @@ namespace KeyboardManagerEditorUI.Interop
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
ExactMatch = mapping.ExactMatch != 0,
});
}
}
@@ -98,6 +99,7 @@ namespace KeyboardManagerEditorUI.Interop
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
ExactMatch = mapping.ExactMatch != 0,
});
}
}
@@ -192,14 +194,14 @@ namespace KeyboardManagerEditorUI.Interop
return KeyboardManagerInterop.AddSingleKeyToTextRemap(_configHandle, originalKey, targetText);
}
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut)
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut, bool exactMatch = false)
{
if (string.IsNullOrEmpty(originalKeys) || string.IsNullOrEmpty(targetKeys))
{
return false;
}
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType);
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType, exactMatch: exactMatch ? 1 : 0);
}
public bool AddShortcutMapping(ShortcutKeyMapping shortcutKeyMapping)
@@ -232,7 +234,8 @@ namespace KeyboardManagerEditorUI.Interop
string.IsNullOrEmpty(shortcutKeyMapping.StartInDirectory) ? null : shortcutKeyMapping.StartInDirectory,
(int)shortcutKeyMapping.Elevation,
(int)shortcutKeyMapping.IfRunningAction,
(int)shortcutKeyMapping.Visibility);
(int)shortcutKeyMapping.Visibility,
shortcutKeyMapping.ExactMatch ? 1 : 0);
}
else if (shortcutKeyMapping.OperationType == ShortcutOperationType.OpenUri)
{
@@ -242,7 +245,8 @@ namespace KeyboardManagerEditorUI.Interop
shortcutKeyMapping.TargetKeys,
shortcutKeyMapping.TargetApp,
(int)shortcutKeyMapping.OperationType,
shortcutKeyMapping.UriToOpen);
shortcutKeyMapping.UriToOpen,
exactMatch: shortcutKeyMapping.ExactMatch ? 1 : 0);
}
return KeyboardManagerInterop.AddShortcutRemap(
@@ -250,7 +254,8 @@ namespace KeyboardManagerEditorUI.Interop
shortcutKeyMapping.OriginalKeys,
shortcutKeyMapping.TargetKeys,
shortcutKeyMapping.TargetApp,
(int)shortcutKeyMapping.OperationType);
(int)shortcutKeyMapping.OperationType,
exactMatch: shortcutKeyMapping.ExactMatch ? 1 : 0);
}
public bool SaveSettings()

View File

@@ -36,6 +36,8 @@ namespace KeyboardManagerEditorUI.Interop
public string UriToOpen { get; set; } = string.Empty;
public bool ExactMatch { get; set; }
public enum ElevationLevel
{
NonElevated = 0,
@@ -79,7 +81,8 @@ namespace KeyboardManagerEditorUI.Interop
Elevation == other.Elevation &&
IfRunningAction == other.IfRunningAction &&
Visibility == other.Visibility &&
UriToOpen == other.UriToOpen;
UriToOpen == other.UriToOpen &&
ExactMatch == other.ExactMatch;
}
public override int GetHashCode()
@@ -97,6 +100,7 @@ namespace KeyboardManagerEditorUI.Interop
hash.Add(IfRunningAction);
hash.Add(Visibility);
hash.Add(UriToOpen);
hash.Add(ExactMatch);
return hash.ToHashCode();
}
}

View File

@@ -76,16 +76,25 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Service Down Banner -->
<InfoBar
x:Name="ServiceDownBanner"
x:Uid="ServiceDownBanner"
Grid.Row="0"
Margin="16,16,16,0"
IsClosable="False"
IsOpen="True"
Severity="Error"
Visibility="Collapsed" />
<StackPanel Grid.Row="0">
<!-- Service Down Banner -->
<InfoBar
x:Name="ServiceDownBanner"
x:Uid="ServiceDownBanner"
Margin="16,16,16,0"
IsClosable="False"
IsOpen="True"
Severity="Error"
Visibility="Collapsed" />
<!-- Orphaned-key notice (non-blocking, shown after a single-key remap) -->
<InfoBar
x:Name="OrphanedKeyBanner"
Margin="16,16,16,0"
IsClosable="True"
IsOpen="False"
Severity="Informational" />
</StackPanel>
<ContentControl
x:Name="MainContentControl"

View File

@@ -39,6 +39,7 @@ namespace KeyboardManagerEditorUI.Pages
private bool _disposed;
private bool _isEditMode;
private EditingItem? _editingItem;
private string? _pendingOrphanedKeyName;
private string _mappingState = "Empty";
private bool _isServiceRunning = true;
@@ -185,6 +186,7 @@ namespace KeyboardManagerEditorUI.Pages
UnifiedMappingControl.SetTriggerKeys(remapping.Shortcut.ToList());
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.KeyOrShortcut);
UnifiedMappingControl.SetActionKeys(remapping.RemappedKeys.ToList());
UnifiedMappingControl.SetExactMatch(remapping.ExactMatch);
UnifiedMappingControl.SetAppSpecific(!remapping.IsAllApps, remapping.AppName);
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
await ShowRemappingDialog();
@@ -210,6 +212,7 @@ namespace KeyboardManagerEditorUI.Pages
UnifiedMappingControl.Reset();
UnifiedMappingControl.SetTriggerKeys(disabledMapping.Shortcut.ToList());
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Disable);
UnifiedMappingControl.SetExactMatch(disabledMapping.ExactMatch);
UnifiedMappingControl.SetAppSpecific(!disabledMapping.IsAllApps, disabledMapping.AppName);
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
await ShowRemappingDialog();
@@ -236,6 +239,7 @@ namespace KeyboardManagerEditorUI.Pages
UnifiedMappingControl.SetTriggerKeys(textMapping.Shortcut.ToList());
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Text);
UnifiedMappingControl.SetTextContent(textMapping.Text);
UnifiedMappingControl.SetExactMatch(textMapping.ExactMatch);
UnifiedMappingControl.SetAppSpecific(!textMapping.IsAllApps, textMapping.AppName);
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
await ShowRemappingDialog();
@@ -274,6 +278,7 @@ namespace KeyboardManagerEditorUI.Pages
UnifiedMappingControl.SetIfRunningAction(mapping.IfRunningAction);
}
UnifiedMappingControl.SetExactMatch(programShortcut.ExactMatch);
UnifiedMappingControl.SetAppSpecific(!programShortcut.IsAllApps, programShortcut.AppName);
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
await ShowRemappingDialog();
@@ -300,6 +305,7 @@ namespace KeyboardManagerEditorUI.Pages
UnifiedMappingControl.SetTriggerKeys(urlShortcut.Shortcut.ToList());
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenUrl);
UnifiedMappingControl.SetUrl(urlShortcut.URL);
UnifiedMappingControl.SetExactMatch(urlShortcut.ExactMatch);
UnifiedMappingControl.SetAppSpecific(!urlShortcut.IsAllApps, urlShortcut.AppName);
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
await ShowRemappingDialog();
@@ -307,17 +313,32 @@ namespace KeyboardManagerEditorUI.Pages
private async System.Threading.Tasks.Task ShowRemappingDialog()
{
_pendingOrphanedKeyName = null;
RemappingDialog.PrimaryButtonClick += RemappingDialog_PrimaryButtonClick;
UnifiedMappingControl.ValidationStateChanged += UnifiedMappingControl_ValidationStateChanged;
RemappingDialog.IsPrimaryButtonEnabled = UnifiedMappingControl.IsInputComplete();
await RemappingDialog.ShowAsync();
ContentDialogResult result = await RemappingDialog.ShowAsync();
RemappingDialog.PrimaryButtonClick -= RemappingDialog_PrimaryButtonClick;
UnifiedMappingControl.ValidationStateChanged -= UnifiedMappingControl_ValidationStateChanged;
_isEditMode = false;
_editingItem = null;
KeyboardHookHelper.Instance.CleanupHook();
if (result == ContentDialogResult.Primary && _pendingOrphanedKeyName != null)
{
ShowOrphanedKeyBanner(_pendingOrphanedKeyName);
}
_pendingOrphanedKeyName = null;
}
private void ShowOrphanedKeyBanner(string keyName)
{
OrphanedKeyBanner.Title = ResourceHelper.GetString("OrphanedKeyInfo_Title");
OrphanedKeyBanner.Message = ResourceHelper.GetString("Warning_OrphanedKeys").Replace("{0}", keyName, System.StringComparison.Ordinal);
OrphanedKeyBanner.IsOpen = true;
}
private void UnifiedMappingControl_ValidationStateChanged(object? sender, EventArgs e)
@@ -382,6 +403,12 @@ namespace KeyboardManagerEditorUI.Pages
return;
}
// If this single-key remap leaves the original key with no assignment, remember it so a
// non-blocking notice can be shown after the dialog closes. Saving is not interrupted.
_pendingOrphanedKeyName = ShouldWarnOrphanedKeys(UnifiedMappingControl.CurrentActionType, triggerKeys, out string orphanedKeyName)
? orphanedKeyName
: null;
if (_isEditMode && _editingItem != null)
{
DeleteExistingMapping();
@@ -404,6 +431,7 @@ namespace KeyboardManagerEditorUI.Pages
}
else
{
_pendingOrphanedKeyName = null;
UnifiedMappingControl.ShowValidationError(ResourceHelper.GetString("Error_SaveFailed_Title"), ResourceHelper.GetString("Error_SaveFailed_Message"));
args.Cancel = true;
}
@@ -443,6 +471,32 @@ namespace KeyboardManagerEditorUI.Pages
};
}
private bool ShouldWarnOrphanedKeys(UnifiedMappingControl.ActionType actionType, List<string> triggerKeys, out string orphanedKeyName)
{
orphanedKeyName = string.Empty;
// Orphaned-key detection mirrors the classic Remap Keys editor: it applies only to
// single-key remaps, where remapping a key away can leave that physical key unreachable.
if (_mappingService == null || triggerKeys.Count != 1 || actionType == UnifiedMappingControl.ActionType.MouseClick)
{
return false;
}
int originalKey = _mappingService.GetKeyCodeFromName(triggerKeys[0]);
if (originalKey == 0)
{
return false;
}
if (ValidationHelper.IsKeyOrphaned(originalKey, _mappingService))
{
orphanedKeyName = triggerKeys[0];
return true;
}
return false;
}
private void DeleteExistingMapping()
{
if (_editingItem == null || _mappingService == null)
@@ -508,7 +562,8 @@ namespace KeyboardManagerEditorUI.Pages
triggerKeys,
actionKeys,
UnifiedMappingControl.GetIsAppSpecific(),
UnifiedMappingControl.GetAppName());
UnifiedMappingControl.GetAppName(),
exactMatch: UnifiedMappingControl.GetExactMatch());
}
private bool SaveDisableMapping(List<string> triggerKeys)
@@ -526,6 +581,7 @@ namespace KeyboardManagerEditorUI.Pages
OriginalKeys = originalKeysString,
TargetKeys = VkDisabledString,
TargetApp = isAppSpecific ? appName : string.Empty,
ExactMatch = UnifiedMappingControl.GetExactMatch(),
};
if (triggerKeys.Count == 1)
@@ -544,7 +600,8 @@ namespace KeyboardManagerEditorUI.Pages
_mappingService!.AddShortcutMapping(
originalKeysString,
VkDisabledString,
isAppSpecific ? appName : string.Empty);
isAppSpecific ? appName : string.Empty,
exactMatch: shortcutKeyMapping.ExactMatch);
}
SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
@@ -597,6 +654,7 @@ namespace KeyboardManagerEditorUI.Pages
private bool SaveShortcutToTextMapping(List<string> triggerKeys, string textContent, bool isAppSpecific, string appName)
{
string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
bool exactMatch = UnifiedMappingControl.GetExactMatch();
var shortcutKeyMapping = new ShortcutKeyMapping
{
@@ -605,11 +663,12 @@ namespace KeyboardManagerEditorUI.Pages
TargetKeys = textContent,
TargetText = textContent,
TargetApp = isAppSpecific ? appName : string.Empty,
ExactMatch = exactMatch,
};
bool saved = isAppSpecific && !string.IsNullOrEmpty(appName)
? _mappingService!.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText)
: _mappingService!.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
? _mappingService!.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText, exactMatch)
: _mappingService!.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText, exactMatch: exactMatch);
if (saved)
{
@@ -637,6 +696,7 @@ namespace KeyboardManagerEditorUI.Pages
TargetKeys = originalKeysString,
UriToOpen = url,
TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
ExactMatch = UnifiedMappingControl.GetExactMatch(),
};
bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
@@ -671,6 +731,7 @@ namespace KeyboardManagerEditorUI.Pages
Visibility = UnifiedMappingControl.GetVisibility(),
Elevation = UnifiedMappingControl.GetElevationLevel(),
TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
ExactMatch = UnifiedMappingControl.GetExactMatch(),
};
bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
@@ -796,7 +857,7 @@ namespace KeyboardManagerEditorUI.Pages
}
else
{
RemappingHelper.SaveMapping(_mappingService!, remapping.Shortcut, remapping.RemappedKeys, !remapping.IsAllApps, remapping.AppName, false);
RemappingHelper.SaveMapping(_mappingService!, remapping.Shortcut, remapping.RemappedKeys, !remapping.IsAllApps, remapping.AppName, exactMatch: remapping.ExactMatch, saveToSettings: false);
}
shortcut.IsActive = true;
@@ -808,7 +869,7 @@ namespace KeyboardManagerEditorUI.Pages
bool saved = shortcut.Shortcut.Count == 1
? _mappingService!.AddSingleKeyToTextMapping(_mappingService.GetKeyCodeFromName(shortcut.Shortcut[0]), shortcutKeyMapping.TargetText)
: shortcutKeyMapping.OperationType == ShortcutOperationType.RemapText
? _mappingService!.AddShortcutMapping(shortcutKeyMapping.OriginalKeys, shortcutKeyMapping.TargetText, operationType: ShortcutOperationType.RemapText)
? _mappingService!.AddShortcutMapping(shortcutKeyMapping.OriginalKeys, shortcutKeyMapping.TargetText, operationType: ShortcutOperationType.RemapText, exactMatch: shortcutKeyMapping.ExactMatch)
: _mappingService!.AddShortcutMapping(shortcutKeyMapping);
if (saved)
@@ -860,7 +921,8 @@ namespace KeyboardManagerEditorUI.Pages
_mappingService!.AddShortcutMapping(
originalKeysString,
VkDisabledString,
!remapping.IsAllApps ? remapping.AppName : string.Empty);
!remapping.IsAllApps ? remapping.AppName : string.Empty,
exactMatch: remapping.ExactMatch);
}
_mappingService!.SaveSettings();
@@ -919,6 +981,7 @@ namespace KeyboardManagerEditorUI.Pages
AppName = mapping.TargetApp ?? string.Empty,
Id = shortcutSettings.Id,
IsActive = shortcutSettings.IsActive,
ExactMatch = mapping.ExactMatch,
};
if (isDisabled)
@@ -957,6 +1020,7 @@ namespace KeyboardManagerEditorUI.Pages
AppName = mapping.TargetApp ?? string.Empty,
Id = shortcutSettings.Id,
IsActive = shortcutSettings.IsActive,
ExactMatch = mapping.ExactMatch,
});
}
}
@@ -991,6 +1055,7 @@ namespace KeyboardManagerEditorUI.Pages
Elevation = mapping.Elevation.ToString(),
IfRunningAction = mapping.IfRunningAction.ToString(),
Visibility = mapping.Visibility.ToString(),
ExactMatch = mapping.ExactMatch,
});
}
}
@@ -1020,6 +1085,7 @@ namespace KeyboardManagerEditorUI.Pages
IsActive = shortcutSettings.IsActive,
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
AppName = mapping.TargetApp ?? string.Empty,
ExactMatch = mapping.ExactMatch,
});
}
}

View File

@@ -486,6 +486,14 @@
<data name="Warning_Title" xml:space="preserve">
<value>Warning</value>
</data>
<data name="Warning_OrphanedKeys" xml:space="preserve">
<value>The key {0} is now remapped, so it can no longer be typed directly unless another key is mapped to it.</value>
<comment>{0} is the name of the key that will be left without an assignment.</comment>
</data>
<data name="OrphanedKeyInfo_Title" xml:space="preserve">
<value>A key is no longer directly available</value>
<comment>Title of the non-blocking notice shown after remapping a single key away.</comment>
</data>
<data name="Error_UnknownValidation_Title" xml:space="preserve">
<value>Validation error</value>
</data>

View File

@@ -22,6 +22,7 @@ public partial class PowerAccent : IDisposable
private const double ScreenMinPadding = 150;
private bool _visible;
private int _showGeneration;
private string[] _characters = Array.Empty<string>();
private string[] _characterDescriptions = Array.Empty<string>();
private int _selectedIndex = -1;
@@ -98,6 +99,10 @@ public partial class PowerAccent : IDisposable
_initialShiftState = WindowsFunctions.IsShiftState();
_visible = true;
// Each summon gets a generation id so a delayed render queued by an earlier
// press can't fire for a newer one (or after the toolbar was hidden).
int generation = ++_showGeneration;
_characters = GetCharacters(letterKey);
_characterDescriptions = GetCharacterDescriptions(_characters);
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
@@ -105,7 +110,7 @@ public partial class PowerAccent : IDisposable
Task.Delay(_settingService.InputTime).ContinueWith(
t =>
{
if (_visible)
if (_visible && generation == _showGeneration)
{
OnChangeDisplay?.Invoke(true, _characters);
}
@@ -237,6 +242,7 @@ public partial class PowerAccent : IDisposable
OnChangeDisplay?.Invoke(false, null);
_selectedIndex = -1;
_visible = false;
_showGeneration++;
}
private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)

View File

@@ -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; }

View File

@@ -207,7 +207,7 @@
<controls:PageLink x:Uid="NewPlus_Learn_More" Link="https://aka.ms/PowerToysOverview_NewPlus" />
</controls:SettingsPageControl.PrimaryLinks>
<controls:SettingsPageControl.SecondaryLinks>
<controls:PageLink Link="https://www.linkedin.com/in/christian-gaardmark/" Text="Christian Gaardmark" />
<controls:PageLink Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt" Text="Based on Christian Gaardmark's New++ from the Productivity Plus Pack" />
</controls:SettingsPageControl.SecondaryLinks>
</controls:SettingsPageControl>

View File

@@ -404,31 +404,26 @@
Name="ZoomItSnipShortcut"
x:Uid="ZoomIt_Snip_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xF7ED;}">
<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=&#xE8AC;}">
<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=&#xE7C5;}"
IsExpanded="True">
HeaderIcon="{ui:FontIcon Glyph=&#xE7C5;}">
<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>

View File

@@ -4432,22 +4432,22 @@ Activate by holding the key for the character you want to add an accent to, then
</data>
<data name="NewPlus.ModuleTitle" xml:space="preserve">
<value>New+</value>
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="NewPlus.ModuleDescription" xml:space="preserve">
<value>Create files and folders from a personalized set of templates</value>
</data>
<data name="NewPlus_Product_Name.Content" xml:space="preserve">
<value>New+</value>
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="NewPlus_Learn_More.Text" xml:space="preserve">
<value>Learn more about New+</value>
<comment>New+ learn more link. Localize product name in accordance with Windows New</comment>
<comment>New+ learn more link. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="NewPlus_Enable_Toggle.Header" xml:space="preserve">
<value>New+</value>
<comment>Localize product name in accordance with Windows New</comment>
<comment>Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="NewPlus_TemplatesNotBackupAndRestoreWarning.Title" xml:space="preserve">
<value>PowerToys "Back up and Restore" feature doesn't take templates into account at this moment. If you use that feature, templates will have to be copied manually.</value>
@@ -4534,7 +4534,7 @@ Activate by holding the key for the character you want to add an accent to, then
</data>
<data name="Oobe_NewPlus.Title" xml:space="preserve">
<value>New+</value>
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
</data>
<data name="Oobe_NewPlus.Description" xml:space="preserve">
<value>Create files and folders from a personalized set of templates.</value>
@@ -4942,11 +4942,11 @@ The break timer font matches the text font.</value>
<data name="ZoomIt_Snip_Shortcut.Header" xml:space="preserve">
<value>Snip activation</value>
</data>
<data name="ZoomIt_Snip_Shortcut_Save" xml:space="preserve">
<value>Press **{0}** to save the snip to a file</value>
<data name="ZoomIt_SnipSave_Shortcut.Header" xml:space="preserve">
<value>Save snip to file</value>
</data>
<data name="ZoomIt_Panorama_Shortcut_Save" xml:space="preserve">
<value>Press **{0}** to save the scrolling screenshot to a file</value>
<data name="ZoomIt_PanoramaSave_Shortcut.Header" xml:space="preserve">
<value>Save scrolling screenshot to file</value>
</data>
<data name="ZoomIt_SnipOcr_Shortcut.Header" xml:space="preserve">
<value>Text recognition and extraction</value>

View File

@@ -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);
}
}