Compare commits

...

24 Commits

Author SHA1 Message Date
Zach Teutsch
df23546c0b [README] Update links and release notes for version 0.98.1 (#46539)
Title.
2026-03-26 15:07:30 -05:00
Alex Mihaiuc
25f44bc6d9 Emulate ZoomIt _mm_cvtsi128_si64 with _mm_storel_epi64 for x86 (#46529)
<!-- 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

Added this to ensure that ZoomIt can still build for 32 bit, even though
PowerToys doesn't ship such binaries.

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

- [ ] Closes: #xxx
<!-- - [ ] 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

Ensured that the ZoomIt subproject compiles fine for 32 bit, this inside
the Sysinternals build process, also tested that the panoramic
screenshot functionality works. This change is isolated through the
preprocessor for AMD64 and ARM64.
2026-03-26 18:33:25 +01:00
Zach Teutsch
dc533fbdb3 [Keyboard Manager] Remove service enable/disable separate from module, fix editor clear shortcut (#46530)
Two changes to shortcuts here:
1) Remove toggling the KBM service with a shortcut or via command
palette
2) Ensure that shortcut is disabled for editor when shortcut is cleared
2026-03-26 10:20:50 -07:00
Zach Teutsch
c05ba4e2c8 [Keyboard Manager] Allow whitespace-only TextRemappings (#46510)
Title.

Closes #46453
2026-03-26 10:14:13 -07:00
Alex Mihaiuc
c83dd972a0 Add ZoomIt panoramic screenshot functionality (#46506)
<!-- 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
This adds several ZoomIt features:

- Panorama / scrolling screenshots. The image reconstruction happens
based on visual cues and accuracy depends on scroll speed during the
capture.
- Text extraction when snipping.
- Break timer improvements (the break timer is now a screen saver,
offering the possibility to lock the computer).
- Functionality for standalone clip trimming is present but not exposed
in the XAML UI.


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

- [ ] Closes: #xxx
<!-- - [ ] 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
- [ ] **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

The build is successful both with PowerToys and as a standalone
Sysinternals executable. We ensured that the features behave as expected
and that no regressions are introduced.

---------

Co-authored-by: Mark Russinovich <markruss@ntdev.microsoft.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: markrussinovich <markrussinovich@users.noreply.github.com>
Co-authored-by: MarioHewardt <marioh@microsoft.com>
2026-03-26 13:21:43 +01:00
previously contributed as ttenbergen
c33053b26b Add missing Icelandic character í (VK_I) (#46424)
## Summary of the Pull Request
The Icelandic language definition was missing `í` entirely. This adds it
to `VK_I`.

Closes: Add missing Icelandic character í (VK_I) #46423

## Validation Steps Performed
Code review only. The change is a single line addition to a data-only
switch statement, consistent in structure with all other language
entries in the file. No binaries, pipelines, or localization files are
affected.
2026-03-26 16:22:44 +08:00
Niels Laute
2cf7d0f5ec Fix for duplicated dockbands (#46438)
<!-- 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

CommandProviderWrapper.PinDockBand had no duplicate check and it blindly
called .Add() on the settings list every time. This allowed the same
extension to be pinned multiple times to the dock. Once duplicates
existed in settings, LoadTopLevelCommands would faithfully re-create all
of them on every CommandsChanged reload, making edit-mode unpin appear
broken (unpin removes one, reload brings them all back).

 **Fix**
- CommandProviderWrapper.PinDockBand: Check all three band lists
(Start/Center/End) for an existing (ProviderId, CommandId) match before
adding; early-return if already pinned.
- CommandProviderWrapper.LoadTopLevelCommands: Track seen command IDs in
a HashSet when iterating AllPinnedCommands; skip duplicates so they
never materialize in the UI even if present in settings.
  
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] 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

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-03-25 12:29:55 -05:00
Copilot
7cb0f3861a CmdPal: Fix duplicate "Pin to..." context commands on top-level items (#46458)
Top-level commands on the home page showed duplicate pin context entries
— e.g., "Pin to Dock" appearing twice, or contradictory "Pin to Dock" +
"Unpin from dock" on the same command.

## Summary of the Pull Request

When the window is hidden while a sub-page is active,
`ShellViewModel.Receive(WindowHiddenMessage)` re-navigates to the root
page while `CurrentPage` still points to the sub-page.
`GetProviderContextForCommand` therefore returns the sub-page's
`ProviderContext` (which has `SupportsPinning = true`, `ProviderId =
<extension>`) for the new home-page `ListViewModel`.

With that wrong context, `UnsafeBuildAndInitMoreCommands` runs for each
`ListItemViewModel` wrapping a `TopLevelViewModel` and injects a second
set of pin commands — using the wrong provider's dock/top-level state —
on top of the ones `TopLevelViewModel.BuildContextMenu()` already
injected via `AddMoreCommandsToTopLevel` with the correct per-item
provider context.

**Changes:**

- **`ShellViewModel.cs` (root cause):** Move `isMainPage` evaluation
before `providerContext` is computed; use `CommandProviderContext.Empty`
when navigating to the root page, regardless of what `CurrentPage` is at
that moment.

  ```csharp
  var isMainPage = command == _rootPage;
  var providerContext = isMainPage
      ? CommandProviderContext.Empty
: _appHostService.GetProviderContextForCommand(message.Context,
CurrentPage.ProviderContext);
  ```

- **`CommandPaletteContextMenuFactory.cs` (defensive guard):** In
`UnsafeBuildAndInitMoreCommands`, bail early when the page context
supports pinning and `commandItem.Model.Unsafe is TopLevelViewModel`.
`BuildContextMenu()` on `TopLevelViewModel` already populates pin
commands via `AddMoreCommandsToTopLevel` with the item's own provider
context; adding them again here is always wrong regardless of how the
page context ended up. The `SupportsPinning` check is evaluated first to
skip the `.Unsafe` type-test in the common non-pinning case.

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

The two fixes are complementary: the `ShellViewModel` change prevents
the wrong `ProviderContext` from ever reaching the home-page
`ListViewModel`; the `CommandPaletteContextMenuFactory` guard ensures
`TopLevelViewModel`-backed items are never double-processed even if some
other future code path sets the page context incorrectly.

The guard in
`CommandPaletteContextMenuFactory.UnsafeBuildAndInitMoreCommands` is
ordered so that `providerContext.SupportsPinning` (a cheap bool property
read) is evaluated before `commandItem.Model.Unsafe is
TopLevelViewModel`. This means the field access and type check are
skipped entirely for the common non-pinning case, addressing reviewer
feedback about unnecessary work on the hot path.

## Validation Steps Performed

Manually reproduced by opening CmdPal, navigating into a sub-page (e.g.,
"Search the Web"), closing the window, reopening, and verifying the
context menu for top-level commands shows exactly one "Pin to Dock" (or
"Unpin from dock") entry and at most one top-level pin action — no
duplicates or contradictory pairs.

<!-- START COPILOT CODING AGENT TIPS -->
---

📍 Connect Copilot coding agent with [Jira](https://gh.io/cca-jira-docs),
[Azure Boards](https://gh.io/cca-azure-boards-docs) or
[Linear](https://gh.io/cca-linear-docs) to delegate work to Copilot in
one click without leaving your project management tool.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: michaeljolley <1228996+michaeljolley@users.noreply.github.com>
2026-03-25 18:16:18 +01:00
Jiří Polášek
1106ac61f5 CmdPal: Guard Frame.GoBack to prevent crash (#46493)
## Summary of the Pull Request

This PR adds a guard to the shell page that prevents navigating back
with empty nav stack (which leads to exception, an ultimately to a
crash).

> You should check that the
[CanGoBack](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.frame.cangoback?view=windows-app-sdk-1.8#microsoft-ui-xaml-controls-frame-cangoback)
property is true before you call GoBack. If you call GoBack while
CanGoBack is false, an exception is thrown.

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

- [x] Closes: #46492
<!-- - [ ] 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-03-25 12:12:11 -05:00
Niels Laute
107bf3882c Make KBM Editor pinnable (#46482) 2026-03-24 18:52:04 -05:00
Jiří Polášek
3f35b11cee CmdPal: Fix missing primary context command for late-bound items (#46131)
This PR does fix a bug where an item that starts with a null or empty
primary command never adds that primary action to the context menu after
the extension later provides a real command.

- Creates the default primary context-menu item lazily when `Command` or
`Command.Name` becomes available after `SlowInitializeProperties()`
- Refreshes `AllCommands`, `SecondaryCommand`, and `HasMoreCommands`
notifications for late command materialization and Show Details updates.
- Adds unit tests to cover the fixed issue.


<!-- 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: #46129 
<!-- - [ ] 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-03-24 17:24:04 -05:00
Jiří Polášek
1a9fcdcd1f CmdPal: Ensure DockWindow property cleans up after itself (#46303)
## Summary of the Pull Request

This PR improves cleanup of DockWindow after itself (since it can be
created and destroyed multiple times during app lifetime).

- Disposes its ViewModel (which it creates).
- Unregisters itself explicitly from WeakReferenceMessenger.
- Ensures that ShellPage closes the dock window when disposed and can't
spawn more.

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

- [x] Closes: #46302
<!-- - [ ] 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

Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
2026-03-24 17:24:00 -05:00
Jiří Polášek
6cf1d32e5a CmdPal: Hotfix commonCallbacks array initial count to prevent negative number (#46215)
## Summary of the Pull Request

This PR ensures that requested initial capacity is not a negative
number. `TopLevelCommandManager.TopLevelCommands` state is not is sync
with `globalFallbacks` here, plus `globalFallbacks` includes providers
that are disabled.

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

- [x] Closes: #46210
<!-- - [ ] 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

---------

Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Co-authored-by: Zachary Teutsch <zteutsch@microsoft.com>
2026-03-24 21:15:22 +00:00
Copilot
33497e59cc Update 'Ignore Shortcut' text to 'Ignore Conflict' for clarity (#46318)
## Summary of the Pull Request

Updates the checkbox label in the ShortcutConflictWindow (shown in the
Settings Dashboard when a hotkey conflict is detected) from "Ignore
shortcut" to "Ignore conflict". This change clarifies that checking the
box ignores the *conflict*, not the shortcut itself.

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

**File changed:**
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`

- Updated `ShortcutConflictWindow_IgnoreShortcut.Content` from `"Ignore
shortcut"` to `"Ignore conflict"`.

The checkbox appears in the ShortcutConflictWindow header row next to
the conflicting hotkey. The previous label "Ignore shortcut" was
ambiguous — it was unclear whether it meant "ignore the conflict" (allow
the shortcut to coexist) or "ignore the shortcut" (disable it). The new
label "Ignore conflict" makes the intent unambiguous.

## Validation Steps Performed

- Manually verified the resource string change in `Resources.resw`.
- Confirmed the `x:Uid="ShortcutConflictWindow_IgnoreShortcut"` binding
in `ShortcutConflictWindow.xaml` picks up the updated `.Content` value.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>"Ignore Shortcut" is ambiguous; Suggest change to "Ignore
Conflict"</issue_title>
> <issue_description>### Description of the new feature / enhancement
> 
> When ignoring shortcut conflicts, it is unclear (to me) if the "Ignore
shortcut" check box "ignores the conflict" (good) or "ignores the
shortcut" ... so it can't be used (bad). A change to the wording to
"Ignore Conflict" would clarify the intent.
> 
> ### Scenario when this would be used?
> 
> Renaming avoids ambiguity and avoids (me) wasting time checking the
AIs for what the check box actually does.
> 
> ### Supporting information
> 
> Not needed (I think)</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes microsoft/PowerToys#46296

<!-- START COPILOT CODING AGENT TIPS -->
---

📱 Kick off Copilot coding agent tasks wherever you are with [GitHub
Mobile](https://gh.io/cca-mobile-docs), available on iOS and Android.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-03-24 22:12:05 +01:00
Niels Laute
3d2f069c43 [CmdPal Dock] New pin UX (#46436)
<!-- 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

This PR introduces a new dialog that gives you more control on how a
command gets pinned to the Dock.


![PinUX](https://github.com/user-attachments/assets/c270e93f-3fd9-42d5-aaa9-95c08efb8bac)


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

- [x] Closes: #46433
<!-- - [ ] 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-03-24 15:19:28 -05:00
Jiří Polášek
79d9b0e667 CmdPal: Keep TimeDateExtensionPage simple and update every time (#46396)
## Summary of the Pull Request

This PR updates Time & Date extension page to calculate current results
every time. This breaks possible infinite loop.

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

- [x] Closes: #46329
<!-- - [ ] 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-03-24 14:51:04 -05:00
Jiří Polášek
e2f611a7fc CmdPal: Prevent PgUp/PgDown from selecting non-internactive items (#46439)
## Summary of the Pull Request

This PR prevents paging (<kbd>PgUp</kbd>/<kbd>PgDown</kbd>) in the item
list from selecting non-interactive items (such as separators or section
headers).

It adds `FindSelectableIndex` and `FindSelectableIndexForPageNavigation`
helper methods, which locate the next interactive item in the given
direction. These methods are used to guard paging navigation and to
consolidate related logic elsewhere.

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

- [x] Closes: #46283 
<!-- - [ ] 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-03-24 14:15:44 -05:00
Jiří Polášek
84ce86c573 CmdPal: Fix missing app context menu actions on the main page (#46293)
## Summary of the Pull Request

This PR fixes missing _Pin to ..._ menu items on app search result.

`MainListPage` reuses `AppListItem` instances from the All Apps page,
but their context menus were being built with the main page provider
context instead of the All Apps provider context.


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

- [x] Closes: #45848
- [x] Closes: #46285
<!-- - [ ] 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-03-24 13:30:52 -05:00
Jiří Polášek
735ea01a93 CmdPal: Fix dock popup XamlRoot handling on DockControl (#46305)
## Summary of the Pull Request

This PR handles situations when app can crash because a popup control is
being touched before XamlRoot is set.

- Registers message handlers in DockControl only while controls are
loaded.
- Guards the edit-mode TeachingTip until the dock is rooted.
- Sets XamlRoot for dock flyouts and tooltips before showing.
- Ensures that tooltips in DockItemControl are set only after XamlRoot
is explicitly set.
- Unregisteres messages and CenterItems_CollectionChanged when unloaded.

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

- [x] Closes: #46228
<!-- - [ ] 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-03-24 14:18:59 -04:00
Jiří Polášek
93f80f5f61 CmdPal: Reduce DockWindow backdrop switching and visual artifacts (#46309)
<!-- 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

This PR reduces "blinking" of dock when (any) CmdPal settings changes.
It handles only backdrop, not icons.

- Avoids recreating the acrylic controller when the effective backdrop
parameters have not changed.
- Reuses the transparent backdrop instead of reassigning it during dock
refreshes.
- Cleans up backdrop controllers only when switching backdrop modes or
disposing the window.
- Removes obsolete dock-specific backdrop helper logic now handled
directly in DockWindow.


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

- [x] Closes: #46308
<!-- - [ ] 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-03-24 14:17:51 -04:00
Kai Tao
21f06b8bd0 Always On Top: The opacity should be able to configure the hotkey individually (#46410)
<!-- 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
This pull request adds support for customizing the hotkeys used to
increase and decrease the opacity of pinned windows in the Always On Top
module.
Previously, these shortcuts were hardcoded to use the same modifiers as
the main pin hotkey.

With these changes, users can now independently configure the increase
and decrease opacity shortcuts via the settings UI, and the backend has
been updated to respect and store these new settings.

Another change: If window is not Always On Topped, the opacity change
take no effect, so we should not intercept, we should pass through to
minimize the impact.

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

- [X] Closes: #46391, #46387
<!-- - [ ] 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
<img width="1184" height="351" alt="image"
src="https://github.com/user-attachments/assets/5d20ffae-9f0c-4ce3-9d85-2ba1efea6301"
/>

<img width="336" height="244" alt="image"
src="https://github.com/user-attachments/assets/a78cc4a3-9eb3-49f1-bbb9-d6db37554e53"
/>

Verified locally that transparency hotkey will not intercept the normal
hotkey in window if Always on top not enabled

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-03-24 14:02:36 -04:00
Dave Rayment
fa78cc8ea7 [OOBE] Ensure the Settings button on the SCOOBE page opens Home, not a blank page (#46203)
## Summary of the Pull Request
This PR fixes an issue where selecting the **Settings** button on the
What's New page for a new or upgraded installation of PowerToys would
show the Settings application but with a blank contents page instead of
the Home page.

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

- [x] Closes: #46202
<!-- - [ ] 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
When the current version of PowerToys doesn't match the
`last_version_run`, the What's New (SCOOBE) page is displayed. The
Settings page is loaded at the same time in a hidden state.

If the user selects the Settings button in the bottom-left of the What's
New page, `OpenSettingsItem_Tapped()` is called, which calls:

```csharp
App.OpenSettingsWindow();
```

This unhides the Settings window, but Settings has not navigated to an
initial page, resulting in a blank display.

The solution is to instead call:

```csharp
App.OpenSettingsWindow(ensurePageIsSelected: true);
```

## Validation Steps Performed

Manual tests, following the instructions given in the original issue,
i.e. setting the `last_version_run` JSON manually and retrying to
simulate the upgrade/new install.
2026-03-24 16:33:19 +00:00
Jessica Dene Earley-Cha
cb9d54317a Add ItemsRepeater focus restoration on Extensions settings page (part deux) (#45903)
<!-- 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 focus management in the Command Palette Extensions settings page.
After user moves through the extension list, when using Shift or
Shift+Tab to navigate into the extensions list, focus now properly
returns to the previously selected extension card instead of jumping to
the first item or end of the list.

This is part of the a11y bug batch.
User Impact:
Keyboard-only and assistive-technology users may lose context and
experience confusion due to unexpected focus movement, increasing
navigation effort and reducing usability of the Extensions page.

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

- [ ] Closes: #xxx
<!-- - [ ] 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
- [ ] **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



https://github.com/user-attachments/assets/2ebe25e4-015d-4804-8ae9-9a0107f39b8e

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-03-24 09:00:05 -07:00
Jiří Polášek
5d0eabed15 CmdPal: Fix scroller scrolling and down glyph (#46447)
## Summary of the Pull Request

This PR restores scrolling to scroller (sic!) and updates a glyph on
scroll down button to caret down symbol.

Regressed in https://github.com/microsoft/PowerToys/pull/45873

## Pictures? Pictures!

Updated glyph:

<img width="170" height="59" alt="image"
src="https://github.com/user-attachments/assets/8b81f883-40e0-47b5-9d49-8523bd1b3cfb"
/>

Horizontal scrolling:


https://github.com/user-attachments/assets/a6b682e9-8439-4966-9837-c234fcc986d5

Vertical scrolling:


https://github.com/user-attachments/assets/166e14ed-374c-414b-9005-8cd7f60a48ba



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

## PR Checklist

- [x] Closes: #46441 
<!-- - [ ] 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-03-24 16:42:41 +01:00
86 changed files with 22906 additions and 764 deletions

View File

@@ -1,9 +1,23 @@
accelscroll
acq
adr
Adr
APPLYTOSUBMENUS
AUDCLNT
axisdefer
axisflip
axisstart
bitmaps
BREAKSCR
BUFFERFLAGS
Cands
capturepath
centiseconds
CLASSW
coeffs
coprime
CREATEDIBSECTION
crossfades
Ctl
CTLCOLOR
CTLCOLORBTN
@@ -11,53 +25,163 @@ CTLCOLORDLG
CTLCOLOREDIT
CTLCOLORLISTBOX
CTrim
ddy
DFCS
dlg
dlu
DONTCARE
downsample
DRAWITEM
DRAWITEMSTRUCT
droppedband
Droppedband
dsum
dupburst
dupsegments
DWLP
EDITCONTROL
ENABLEHOOK
expectedlock
fastscroll
FDE
GETCHANNELRECT
GETCHECK
GETSCREENSAVEACTIVE
GETSCREENSAVETIMEOUT
GETTHUMBRECT
GIFs
hcfdark
hcfwhitespace
HTBOTTOMRIGHT
HTHEME
htol
ICONINFORMATION
ICONWARNING
Inj
jumprecover
KSDATAFORMAT
latestcapture
ldx
LEFTNOWORDWRAP
legitjumps
letterbox
lld
llu
llums
logfont
lookback
lround
lte
luma
Luma
manualdrop
maskcache
maxstep
MENUINFO
mic
middledrop
middledrop
MMRESULT
momentumreversal
mrate
mrt
narrowstrip
ncapture
ncm
nduplicates
niterations
nmonitor
NONCLIENTMETRICS
nonvle
nredraw
nstop
nsubpixel
ntorn
nvw
osc
OWNERDRAW
PBGRA
periodictrap
pfdc
playhead
pointerreuse
pwfx
Qpc
quantums
RCZOOMITSCR
realcapture
REFKNOWNFOLDERID
reposted
SCREENSAVE
SCRNSAVE
SCRNSAVECONFIGURE
scrnsavw
Scrnsavw
scrollramp
SCROLLSIZEGRIP
selftest
SETBARCOLOR
SETBKCOLOR
SETDEFID
SETRECT
SETSCREENSAVETIMEOUT
SHAREMODE
SHAREVIOLATION
shortlist
slowthenfast
smallstart
SNIPOCR
ssi
startuprecovery
stf
stopafter
STREAMFLAGS
submix
sxx
sxy
syy
tallportal
tci
tcsicmp
TEXTMETRIC
tinystep
tme
toolbars
TRACKMOUSEEVENT
Unadvise
vaddq
vaddvq
vandq
vcgeq
vdup
vld
vle
Vle
VLE
vminq
vmlal
vmull
vqaddq
vshrn
vsntprintf
vsnwprintf
vsync
WASAPI
WAVEFORMATEX
WAVEFORMATEXTENSIBLE
wfopen
wideportal
wil
WMU
wrapjump
wtol
WTSSESSION
WTSUn
XEnd
XStart
XStep
YInternal
ZMBS
zncc
Zncc
ZNCC

View File

@@ -427,7 +427,7 @@
</Project>
</Folder>
<Folder Name="/modules/FileLocksmith/">
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49D456D3-F485-45AF-8875-45B44F193DDC" />
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49d456d3-f485-45af-8875-45b44f193ddc" />
<Project Path="src/modules/FileLocksmith/FileLocksmithContextMenu/FileLocksmithContextMenu.vcxproj" Id="799a50d8-de89-4ed1-8ff8-ad5a9ed8c0ca" />
<Project Path="src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj" Id="57175ec7-92a5-4c1e-8244-e3fbca2a81de" />
<Project Path="src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj" Id="9d52fd25-ef90-4f9a-a015-91efc5daf54f" />
@@ -438,7 +438,7 @@
</Project>
</Folder>
<Folder Name="/modules/FileLocksmith/Tests/">
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="A1B2C3D4-E5F6-7890-1234-567890ABCDEF" />
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-1234-567890abcdef" />
</Folder>
<Folder Name="/modules/Hosts/">
<Project Path="src/modules/Hosts/Hosts/Hosts.csproj">
@@ -464,13 +464,13 @@
</Folder>
<Folder Name="/modules/imageresizer/">
<Project Path="src/modules/imageresizer/dll/ImageResizerExt.vcxproj" Id="0b43679e-edfa-4da0-ad30-f4628b308b1b" />
<Project Path="src/modules/imageresizer/ImageResizerContextMenu/ImageResizerContextMenu.vcxproj" Id="93b72a06-c8bd-484f-a6f7-c9f280b150bf" />
<Project Path="src/modules/imageresizer/ImageResizerLib/ImageResizerLib.vcxproj" Id="18b3db45-4ffe-4d01-97d6-5223feee1853" />
<Project Path="src/modules/imageresizer/ui/ImageResizerUI.csproj">
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Project Path="src/modules/imageresizer/ImageResizerContextMenu/ImageResizerContextMenu.vcxproj" Id="93b72a06-c8bd-484f-a6f7-c9f280b150bf" />
<Project Path="src/modules/imageresizer/ImageResizerLib/ImageResizerLib.vcxproj" Id="18b3db45-4ffe-4d01-97d6-5223feee1853" />
<Project Path="src/modules/imageresizer/ui/ImageResizerUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
@@ -1027,7 +1027,10 @@
<File Path="src/modules/Workspaces/workspaces-common/WindowUtils.h" />
</Folder>
<Folder Name="/modules/ZoomIt/">
<Project Path="src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj" Id="0a84f764-3a88-44cd-aa96-41bdbd48627b" />
<Project Path="src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj" Id="0a84f764-3a88-44cd-aa96-41bdbd48627b">
<BuildDependency Project="src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj" />
</Project>
<Project Path="src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj" Id="94ba3051-c8d7-454a-9d46-1a7c78e228a3" />
<Project Path="src/modules/ZoomIt/ZoomItModuleInterface/ZoomItModuleInterface.vcxproj" Id="e4585179-2ac1-4d5f-a3ff-cfc5392f694c" />
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
</Folder>

View File

@@ -53,17 +53,17 @@ Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysUserSetup-0.98.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysUserSetup-0.98.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysSetup-0.98.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysSetup-0.98.0-arm64.exe
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.98.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.98.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.98.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.98.0-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.98.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.98.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.98.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.98.1-arm64.exe][ptMachineArm64] |
</details>
@@ -106,7 +106,7 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
[![What's new image](doc/images/readme/Release-Banner.png)](https://github.com/microsoft/PowerToys/releases)
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.0).
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.1).
## 🛣️ Roadmap
We are planning some nice new features and improvements for the next releases PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!

View File

@@ -300,10 +300,6 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT;
}
hstring Constants::ToggleKeyboardManagerActiveEvent()
{
return CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT;
}
hstring Constants::KeyboardManagerEngineInstanceMutex()
{
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;

View File

@@ -78,7 +78,6 @@ namespace winrt::PowerToys::Interop::implementation
static hstring MWBToggleEasyMouseEvent();
static hstring MWBReconnectEvent();
static hstring OpenNewKeyboardManagerEvent();
static hstring ToggleKeyboardManagerActiveEvent();
static hstring KeyboardManagerEngineInstanceMutex();
};
}

View File

@@ -75,7 +75,6 @@ namespace PowerToys
static String MWBToggleEasyMouseEvent();
static String MWBReconnectEvent();
static String OpenNewKeyboardManagerEvent();
static String ToggleKeyboardManagerActiveEvent();
static String KeyboardManagerEngineInstanceMutex();
}
}

View File

@@ -151,6 +151,7 @@ namespace CommonSharedConstants
const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b";
const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d";
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
const wchar_t ZOOMIT_SNIPOCR_EVENT[] = L"Local\\PowerToysZoomIt-SnipOcrEvent-a7c3b1d2-9e4f-4a6b-8d5c-1f2e3a4b5c6d";
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
// Path to the events used by PowerDisplay
@@ -172,7 +173,6 @@ namespace CommonSharedConstants
// Path to events used by Keyboard Manager
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7";
const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex";
// used from quick access window

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
//============================================================================
//
// PanoramaCapture.h
//
// Panorama (scrolling) screen capture and stitching.
//
// Copyright (C) Mark Russinovich
// Sysinternals - www.sysinternals.com
//
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//============================================================================
#pragma once
#include <windows.h>
#include <vector>
// Globals shared with the main ZoomIt module.
extern bool g_PanoramaCaptureActive;
extern bool g_PanoramaStopRequested;
extern bool g_PanoramaDebugMode;
// Run the panorama capture flow: select a region, capture frames while
// scrolling, stitch them together, and copy the result to the clipboard.
bool RunPanoramaCaptureToClipboard( HWND hWnd );
// Run the panorama capture flow and save the result to a file via a
// Save As dialog instead of copying to the clipboard.
bool RunPanoramaCaptureToFile( HWND hWnd );
// Run a synthetic, non-interactive self-test for panorama frame stitching.
// Returns true when stitching output matches expected dimensions/content.
#ifdef _DEBUG
bool RunPanoramaStitchSelfTest();
// Re-stitch frames from a specific debug dump directory.
bool RunPanoramaStitchDumpDirectory( const wchar_t* path );
// Re-stitch accepted panorama frames from the latest debug dump session and
// save output into that same session directory.
bool RunPanoramaStitchLatestDebugDump();
#endif

View File

@@ -11,6 +11,23 @@
#include "Utility.h"
#include "WindowsVersions.h"
static void SelectRectangleDebugLog( const wchar_t* format, ... )
{
#if _DEBUG
wchar_t message[1024]{};
va_list args;
#pragma warning( push )
#pragma warning( disable : 26492 )
va_start( args, format );
#pragma warning( pop )
vswprintf_s( message, format, args );
va_end( args );
OutputDebugStringW( message );
#else
UNREFERENCED_PARAMETER( format );
#endif
}
//----------------------------------------------------------------------------
//
// SelectRectangle::Start
@@ -18,6 +35,12 @@
//----------------------------------------------------------------------------
bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
{
m_stopping = false;
SelectRectangleDebugLog( L"[SelectRectangle] Start owner=%p fullMonitor=%d minSize=%d alpha=%u\n",
ownerWindow,
fullMonitor ? 1 : 0,
MinSize(),
Alpha() );
WNDCLASSW windowClass{};
windowClass.lpfnWndProc = []( HWND window, UINT message, WPARAM wordParam, LPARAM longParam ) -> LRESULT
{
@@ -46,10 +69,16 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
m_cancel = false;
auto rect = GetMonitorRectFromCursor();
SelectRectangleDebugLog( L"[SelectRectangle] Monitor rect=(%ld,%ld)-(%ld,%ld)\n",
rect.left,
rect.top,
rect.right,
rect.bottom );
m_window = wil::unique_hwnd( CreateWindowExW( WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, m_className, nullptr, WS_POPUP,
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, ownerWindow,
nullptr, nullptr, this ) );
THROW_LAST_ERROR_IF_NULL( m_window.get() );
SelectRectangleDebugLog( L"[SelectRectangle] Window created hwnd=%p\n", m_window.get() );
if( fullMonitor )
{
@@ -58,7 +87,11 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
}
else
{
SetLayeredWindowAttributes( m_window.get(), 0, Alpha(), LWA_ALPHA );
const BOOL layered = SetLayeredWindowAttributes( m_window.get(), 0, Alpha(), LWA_ALPHA );
SelectRectangleDebugLog( L"[SelectRectangle] SetLayeredWindowAttributes(alpha=%u) success=%d err=%lu\n",
Alpha(),
layered ? 1 : 0,
layered ? 0 : GetLastError() );
}
ShowWindow( m_window.get(), SW_SHOW );
@@ -69,6 +102,7 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
GetClipCursor( &m_oldClipRect );
ClipCursor( &rect );
m_setClip = true;
SelectRectangleDebugLog( L"[SelectRectangle] Cursor clipped to monitor bounds\n" );
}
MSG message;
@@ -78,13 +112,20 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
DispatchMessageW( &message );
if( m_cancel )
{
SelectRectangleDebugLog( L"[SelectRectangle] Start cancelled via Stop()\n" );
return false;
}
if( m_selected )
{
SelectRectangleDebugLog( L"[SelectRectangle] Selection finalized rect=(%ld,%ld)-(%ld,%ld)\n",
m_selectedRect.left,
m_selectedRect.top,
m_selectedRect.right,
m_selectedRect.bottom );
break;
}
}
SelectRectangleDebugLog( L"[SelectRectangle] Start complete selected=%d cancel=%d\n", m_selected ? 1 : 0, m_cancel ? 1 : 0 );
return true;
}
@@ -95,15 +136,38 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
//----------------------------------------------------------------------------
void SelectRectangle::Stop()
{
if( m_stopping )
{
SelectRectangleDebugLog( L"[SelectRectangle] Stop ignored due to reentrancy\n" );
return;
}
m_stopping = true;
SelectRectangleDebugLog( L"[SelectRectangle] Stop hwnd=%p selected=%d cancel=%d clip=%d rect=(%ld,%ld)-(%ld,%ld)\n",
m_window.get(),
m_selected ? 1 : 0,
m_cancel ? 1 : 0,
m_setClip ? 1 : 0,
m_selectedRect.left,
m_selectedRect.top,
m_selectedRect.right,
m_selectedRect.bottom );
if( m_setClip )
{
ClipCursor( &m_oldClipRect );
m_setClip = false;
}
m_window.reset();
HWND window = m_window.release();
if( window != nullptr && IsWindow( window ) )
{
DestroyWindow( window );
}
m_selected = false;
m_selectedRect = {};
m_cancel = true;
m_stopping = false;
}
//----------------------------------------------------------------------------
@@ -114,11 +178,20 @@ void SelectRectangle::Stop()
void SelectRectangle::ShowSelected()
{
m_selected = true;
SelectRectangleDebugLog( L"[SelectRectangle] ShowSelected rect=(%ld,%ld)-(%ld,%ld) dpi=%u\n",
m_selectedRect.left,
m_selectedRect.top,
m_selectedRect.right,
m_selectedRect.bottom,
m_dpi );
// Set the alpha to match the Windows graphics capture API yellow border
// and set the window to be transparent and disabled, so it will be skipped
// for hit testing and as a candidate for the next foreground window.
SetLayeredWindowAttributes( m_window.get(), 0, 191, LWA_ALPHA );
const BOOL layered = SetLayeredWindowAttributes( m_window.get(), 0, 191, LWA_ALPHA );
SelectRectangleDebugLog( L"[SelectRectangle] ShowSelected SetLayeredWindowAttributes(alpha=191) success=%d err=%lu\n",
layered ? 1 : 0,
layered ? 0 : GetLastError() );
SetWindowLong( m_window.get(), GWL_EXSTYLE, GetWindowLong( m_window.get(), GWL_EXSTYLE ) | WS_EX_TRANSPARENT );
EnableWindow( m_window.get(), FALSE );
@@ -144,6 +217,12 @@ void SelectRectangle::ShowSelected()
point.x += windowRect.left;
point.y += windowRect.top;
MoveWindow( m_window.get(), point.x, point.y, rect.right, rect.bottom, true );
SelectRectangleDebugLog( L"[SelectRectangle] Border window moved to (%ld,%ld) size=%ldx%ld borderWidth=%d\n",
point.x,
point.y,
rect.right,
rect.bottom,
width );
// Use a region to keep everything but the border transparent.
wil::unique_hrgn region{CreateRectRgnIndirect( &rect )};
@@ -151,6 +230,11 @@ void SelectRectangle::ShowSelected()
wil::unique_hrgn insideRegion{CreateRectRgnIndirect( &rect )};
CombineRgn( region.get(), region.get(), insideRegion.get(), RGN_XOR );
SetWindowRgn( m_window.get(), region.release(), true );
SelectRectangleDebugLog( L"[SelectRectangle] Border window region applied\n" );
// Force immediate paint so the yellow border is visible instead of a
// transient black frame from the class background brush.
RedrawWindow( m_window.get(), nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_FRAME );
}
//----------------------------------------------------------------------------
@@ -162,6 +246,7 @@ void SelectRectangle::UpdateOwner( HWND window )
{
if( m_window != nullptr )
{
SelectRectangleDebugLog( L"[SelectRectangle] UpdateOwner hwnd=%p newOwner=%p\n", m_window.get(), window );
SetWindowLongPtr( m_window.get(), GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(window) );
SetWindowPos( m_window.get(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE );
}
@@ -179,10 +264,24 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
case WM_CREATE:
m_dpi = GetDpiForWindowHelper( window );
SetWindowDisplayAffinity( window, WDA_EXCLUDEFROMCAPTURE );
SelectRectangleDebugLog( L"[SelectRectangle] WM_CREATE hwnd=%p dpi=%u\n", window, m_dpi );
return 0;
case WM_DESTROY:
Stop();
SelectRectangleDebugLog( L"[SelectRectangle] WM_DESTROY hwnd=%p\n", window );
if( m_window.get() == window )
{
m_window.release();
}
if( m_setClip )
{
ClipCursor( &m_oldClipRect );
m_setClip = false;
}
m_selected = false;
m_selectedRect = {};
m_cancel = true;
m_stopping = false;
return 0;
case WM_LBUTTONDOWN:
@@ -190,6 +289,7 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
SetCapture( window );
m_startPoint = { GET_X_LPARAM( longParam ), GET_Y_LPARAM( longParam ) };
SelectRectangleDebugLog( L"[SelectRectangle] WM_LBUTTONDOWN startPoint=(%ld,%ld)\n", m_startPoint.x, m_startPoint.y );
[[fallthrough]];
}
case WM_MOUSEMOVE:
@@ -199,6 +299,11 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
GetClientRect( window, &rect );
POINT point{ GET_X_LPARAM( longParam ), GET_Y_LPARAM( longParam ) };
m_selectedRect = ForceRectInBounds( RectFromPointsMinSize( m_startPoint, point, MinSize() ), rect );
SelectRectangleDebugLog( L"[SelectRectangle] Drag rect=(%ld,%ld)-(%ld,%ld)\n",
m_selectedRect.left,
m_selectedRect.top,
m_selectedRect.right,
m_selectedRect.bottom );
// Use a region to carve out the selected rectangle.
wil::unique_hrgn region{CreateRectRgnIndirect( &m_selectedRect )};
@@ -211,6 +316,7 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
case WM_KEYDOWN:
if( wordParam == VK_ESCAPE )
{
SelectRectangleDebugLog( L"[SelectRectangle] WM_KEYDOWN Escape pressed\n" );
Stop();
}
return 0;
@@ -218,12 +324,18 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
case WM_KILLFOCUS:
if( !m_selected )
{
SelectRectangleDebugLog( L"[SelectRectangle] WM_KILLFOCUS before selection complete\n" );
Stop();
}
return 0;
case WM_LBUTTONUP:
{
SelectRectangleDebugLog( L"[SelectRectangle] WM_LBUTTONUP selectedRect=(%ld,%ld)-(%ld,%ld)\n",
m_selectedRect.left,
m_selectedRect.top,
m_selectedRect.right,
m_selectedRect.bottom );
if( m_setClip )
{
ClipCursor( &m_oldClipRect );
@@ -249,6 +361,11 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
RECT rect;
GetClientRect( window, &rect );
SelectRectangleDebugLog( L"[SelectRectangle] WM_PAINT selected border rect=(%ld,%ld)-(%ld,%ld)\n",
rect.left,
rect.top,
rect.right,
rect.bottom );
// Draw a border matching the Windows graphics capture API border.
// The outer frame is yellow and two logical pixels wide, while the

View File

@@ -20,10 +20,14 @@ public:
void MinSize( int minSize ) { m_minSize = minSize; }
int MinSize() const { return m_minSize; }
RECT SelectedRect() const { return m_selectedRect; }
bool IsActive() const { return m_window != nullptr; }
bool Start( HWND ownerWindow = nullptr, bool fullMonitor = false );
void Stop();
void UpdateOwner( HWND window );
void Hide() { if( m_window ) ShowWindow( m_window.get(), SW_HIDE ); }
void Show() { if( m_window ) ShowWindow( m_window.get(), SW_SHOWNA ); }
void SetExcludeFromCapture( bool exclude ) { if( m_window ) SetWindowDisplayAffinity( m_window.get(), exclude ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE ); }
private:
BYTE m_alpha = 176;
@@ -36,6 +40,7 @@ private:
RECT m_oldClipRect{};
bool m_selected{ false };
bool m_setClip{ false };
bool m_stopping{ false };
POINT m_startPoint{};
wil::unique_hwnd m_window;

View File

@@ -406,7 +406,10 @@ static bool LoadGifFrames(const std::wstring& gifPath, VideoRecordingSession::Tr
const auto& lastFrame = pData->gifFrames.back();
pData->videoDuration = winrt::TimeSpan{ lastFrame.start.count() + lastFrame.duration.count() };
pData->trimEnd = pData->videoDuration;
if( pData->trimEnd.count() <= 0 )
{
pData->trimEnd = pData->videoDuration;
}
pData->gifFramesLoaded = true;
pData->gifLastFrameIndex = 0;
@@ -721,13 +724,9 @@ namespace
SetDlgItemText(hDlg, IDC_TRIM_DURATION_LABEL, durationText.c_str());
}
// Enable OK when trimming is active (even if unchanged since dialog opened),
// or when the user changed the selection (including reverting to full length).
const bool trimChanged = (pData->trimStart.count() != pData->originalTrimStart.count()) ||
(pData->trimEnd.count() != pData->originalTrimEnd.count());
const bool trimIsActive = (pData->trimStart.count() > 0) ||
(pData->videoDuration.count() > 0 && pData->trimEnd.count() < pData->videoDuration.count());
EnableWindow(GetDlgItem(hDlg, IDOK), trimChanged || trimIsActive);
// Always enable OK so users can close the dialog after previewing
// without being forced to use Cancel.
EnableWindow(GetDlgItem(hDlg, IDOK), TRUE);
}
RECT GetTimelineTrackRect(const RECT& clientRect, UINT dpi)
@@ -1345,7 +1344,10 @@ public:
auto trimResult = VideoRecordingSession::ShowTrimDialog(hParent, m_videoPath, *m_pTrimStart, *m_pTrimEnd);
if (trimResult == IDOK)
{
*m_pShouldTrim = true;
// Trim values are only written back when the user actually
// changed the selection, so a non-zero trimEnd means a
// real trim is requested.
*m_pShouldTrim = (m_pTrimEnd->count() > 0);
}
else if( trimResult == IDCANCEL )
{
@@ -1502,12 +1504,13 @@ INT_PTR VideoRecordingSession::ShowTrimDialog(
HWND hParent,
const std::wstring& videoPath,
winrt::TimeSpan& trimStart,
winrt::TimeSpan& trimEnd)
winrt::TimeSpan& trimEnd,
bool standaloneMode)
{
std::promise<INT_PTR> resultPromise;
auto resultFuture = resultPromise.get_future();
std::thread staThread([hParent, videoPath, &trimStart, &trimEnd, promise = std::move(resultPromise)]() mutable
std::thread staThread([hParent, videoPath, &trimStart, &trimEnd, standaloneMode, promise = std::move(resultPromise)]() mutable
{
bool coInitialized = false;
try
@@ -1525,7 +1528,7 @@ INT_PTR VideoRecordingSession::ShowTrimDialog(
try
{
INT_PTR dlgResult = ShowTrimDialogInternal(hParent, videoPath, trimStart, trimEnd);
INT_PTR dlgResult = ShowTrimDialogInternal(hParent, videoPath, trimStart, trimEnd, standaloneMode);
promise.set_value(dlgResult);
}
catch (const winrt::hresult_error& e)
@@ -1584,7 +1587,8 @@ INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
HWND hParent,
const std::wstring& videoPath,
winrt::TimeSpan& trimStart,
winrt::TimeSpan& trimEnd)
winrt::TimeSpan& trimEnd,
bool standaloneMode)
{
TrimDialogData data;
data.videoPath = videoPath;
@@ -1592,6 +1596,7 @@ INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
data.trimStart = trimStart;
data.trimEnd = trimEnd;
data.isGif = IsGifPath(videoPath);
data.standaloneMode = standaloneMode;
if (data.isGif)
{
@@ -1786,8 +1791,17 @@ INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
if (result == IDOK)
{
trimStart = data.trimStart;
trimEnd = data.trimEnd;
// Only write back trim values when the user actually changed the
// selection. This lets the caller distinguish "confirmed without
// trimming" (preview-only) from a real trim operation.
const bool selectionChanged =
(data.trimStart.count() != data.originalTrimStart.count()) ||
(data.trimEnd.count() != data.originalTrimEnd.count());
if (selectionChanged)
{
trimStart = data.trimStart;
trimEnd = data.trimEnd;
}
}
return result;
@@ -3890,6 +3904,12 @@ INT_PTR CALLBACK VideoRecordingSession::TrimDialogProc(HWND hDlg, UINT message,
// Make OK the default button
SendMessage(hDlg, DM_SETDEFID, IDOK, 0);
// In standalone mode, change OK button text to "Save As"
if (pData->standaloneMode)
{
SetDlgItemText(hDlg, IDOK, L"Save As");
}
// Subclass the dialog to handle resize grip hit testing
SetWindowSubclass(hDlg, TrimDialogSubclassProc, 0, reinterpret_cast<DWORD_PTR>(pData));
@@ -5029,6 +5049,115 @@ INT_PTR CALLBACK VideoRecordingSession::TrimDialogProc(HWND hDlg, UINT message,
case IDOK:
pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
StopPlayback(hDlg, pData);
if (pData->standaloneMode)
{
// In standalone mode, "Save As" shows a save dialog and performs the trim
auto saveDialog = wil::CoCreateInstance<::IFileSaveDialog>(CLSID_FileSaveDialog);
FILEOPENDIALOGOPTIONS options;
if (SUCCEEDED(saveDialog->GetOptions(&options)))
saveDialog->SetOptions(options | FOS_FORCEFILESYSTEM);
wil::com_ptr<::IShellItem> videosItem;
if (SUCCEEDED(SHGetKnownFolderItem(FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr,
IID_IShellItem, (void**)videosItem.put())))
saveDialog->SetDefaultFolder(videosItem.get());
// Derive suggested filename from source
std::wstring suggestedName;
{
auto pos = pData->videoPath.find_last_of(L"\\/");
suggestedName = (pos != std::wstring::npos) ? pData->videoPath.substr(pos + 1) : pData->videoPath;
auto dot = suggestedName.find_last_of(L'.');
if (dot != std::wstring::npos)
suggestedName.insert(dot, L"_trimmed");
else
suggestedName += L"_trimmed";
}
if (pData->isGif)
{
saveDialog->SetDefaultExtension(L".gif");
COMDLG_FILTERSPEC fileTypes[] = { { L"GIF Animation", L"*.gif" } };
saveDialog->SetFileTypes(_countof(fileTypes), fileTypes);
}
else
{
saveDialog->SetDefaultExtension(L".mp4");
COMDLG_FILTERSPEC fileTypes[] = { { L"MP4 Video", L"*.mp4" } };
saveDialog->SetFileTypes(_countof(fileTypes), fileTypes);
}
saveDialog->SetFileName(suggestedName.c_str());
saveDialog->SetTitle(L"ZoomIt: Save Trimmed Video As...");
HRESULT hr = saveDialog->Show(hDlg);
if (FAILED(hr))
{
// User cancelled save dialog — return to trim editor
return TRUE;
}
wil::com_ptr<::IShellItem> resultItem;
THROW_IF_FAILED(saveDialog->GetResult(resultItem.put()));
wil::unique_cotaskmem_string savePath;
THROW_IF_FAILED(resultItem->GetDisplayName(SIGDN_FILESYSPATH, savePath.put()));
// Capture what we need before closing the dialog
std::wstring videoPath = pData->videoPath;
bool isGif = pData->isGif;
auto trimStart = pData->trimStart;
auto trimEnd = pData->trimEnd;
std::wstring savePathStr(savePath.get());
// Close the trim dialog immediately
EndDialog(hDlg, IDOK);
// Perform the trim after the dialog is closed
try
{
auto trimOp = isGif
? TrimGifAsync(videoPath, trimStart, trimEnd)
: TrimVideoAsync(videoPath, trimStart, trimEnd);
// Pump messages while waiting for async operation
while (trimOp.Status() == winrt::AsyncStatus::Started)
{
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Sleep(10);
}
auto trimmedPath = std::wstring(trimOp.GetResults());
if (trimmedPath.empty())
{
MessageBox(nullptr, L"Failed to trim video.", L"Error", MB_OK | MB_ICONERROR);
return TRUE;
}
// Copy trimmed file to the user-chosen save location
if (!CopyFile(trimmedPath.c_str(), savePathStr.c_str(), FALSE))
{
MessageBox(nullptr, L"Failed to save the trimmed file.", L"Error", MB_OK | MB_ICONERROR);
DeleteFile(trimmedPath.c_str());
return TRUE;
}
// Clean up temp file
DeleteFile(trimmedPath.c_str());
}
catch (...)
{
MessageBox(nullptr, L"Failed to trim video.", L"Error", MB_OK | MB_ICONERROR);
}
return TRUE;
}
// Trim times are already set by mouse dragging
EndDialog(hDlg, IDOK);
return TRUE;

View File

@@ -132,6 +132,7 @@ public:
bool isDragging{ false };
int lastPlayheadX{ -1 }; // Track last playhead pixel position for efficient invalidation
MMRESULT mmTimerId{ 0 }; // Multimedia timer for smooth MP4 playback
bool standaloneMode{ false }; // When true, OK becomes "Save As" and handles file saving directly
// Helper to convert time to pixel position
int TimeToPixel(winrt::Windows::Foundation::TimeSpan time, int timelineWidth) const
@@ -162,7 +163,8 @@ public:
HWND hParent,
const std::wstring& videoPath,
winrt::Windows::Foundation::TimeSpan& trimStart,
winrt::Windows::Foundation::TimeSpan& trimEnd);
winrt::Windows::Foundation::TimeSpan& trimEnd,
bool standaloneMode = false);
private:
static INT_PTR CALLBACK TrimDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
@@ -179,7 +181,8 @@ private:
HWND hParent,
const std::wstring& videoPath,
winrt::Windows::Foundation::TimeSpan& trimStart,
winrt::Windows::Foundation::TimeSpan& trimEnd);
winrt::Windows::Foundation::TimeSpan& trimEnd,
bool standaloneMode = false);
private:
VideoRecordingSession(

View File

@@ -113,26 +113,26 @@ END
// Dialog
//
OPTIONS DIALOGEX 0, 0, 299, 325
OPTIONS DIALOGEX 0, 0, 299, 331
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_CONTROLPARENT
CAPTION "ZoomIt - Sysinternals: www.sysinternals.com"
FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
LTEXT "ZoomIt v10.1",IDC_VERSION,42,7,73,10
DEFPUSHBUTTON "OK",IDOK,184,308,50,14
PUSHBUTTON "Cancel",IDCANCEL,241,308,50,14
LTEXT "ZoomIt v11.0",IDC_VERSION,42,7,73,10
LTEXT "Copyright \251 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,295,105,10
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,285,247
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,309,122,10
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,302,105,10
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,45,285,255
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,316,122,10
END
ADVANCED_BREAK DIALOGEX 0, 0, 209, 225
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
ADVANCED_BREAK DIALOGEX 0, 0, 209, 223
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Advanced Break Options"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
@@ -158,8 +158,8 @@ BEGIN
EDITTEXT IDC_BACKGROUND_FILE,62,164,125,12,ES_AUTOHSCROLL | ES_READONLY
PUSHBUTTON "&...",IDC_BACKGROUND_BROWSE,188,164,13,11
CONTROL "Scale to screen:",IDC_CHECK_BACKGROUND_STRETCH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,58,180,67,10,WS_EX_RIGHT
DEFPUSHBUTTON "OK",IDOK,97,199,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,199,50,14
DEFPUSHBUTTON "OK",IDOK,97,202,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,202,50,14
LTEXT "Alarm Sound File:",IDC_STATIC_SOUND_FILE,61,26,56,8
LTEXT "Timer Opacity:",IDC_STATIC,8,59,48,8
LTEXT "Timer Position:",IDC_STATIC,8,77,48,8
@@ -215,21 +215,23 @@ BEGIN
GROUPBOX "Sample",IDC_TEXT_FONT,8,61,99,28
END
BREAK DIALOGEX 0, 0, 260, 123
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
BREAK DIALOGEX 0, 0, 260, 159
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,67,80,12
EDITTEXT IDC_TIMER,52,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,86,11,12
LTEXT "minutes",IDC_STATIC,88,88,25,8
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,192,102,41,14
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,230,33
LTEXT "Start Timer:",IDC_STATIC,7,70,39,8
LTEXT "Timer:",IDC_STATIC,7,88,20,8
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,230,20
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,74,80,12
EDITTEXT IDC_TIMER,52,93,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,93,11,12
LTEXT "minutes",IDC_STATIC,88,95,25,8
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,213,140,41,14
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,242,33
LTEXT "Start Timer:",IDC_STATIC,7,77,39,8
LTEXT "Timer:",IDC_STATIC,7,95,20,8
LTEXT "Change the break timer color using the same keys that the drawing color, including background color. The break timer font is the same as text font.",IDC_STATIC,7,45,241,26
CONTROL "Show Time Elapsed After Expiration:",IDC_CHECK_SHOW_EXPIRED,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,8,104,132,10
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,111,130,10
CONTROL "Lock Workstation During Break:",IDC_CHECK_LOCK_WORKSTATION,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,126,113,10
END
1543 DIALOGEX 100, 50, 216, 131
@@ -249,19 +251,19 @@ BEGIN
CTEXT "AaBbYyZz",1092,16,88,127,31,SS_NOPREFIX | NOT WS_VISIBLE
END
LIVEZOOM DIALOGEX 0, 0, 260, 134
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
LIVEZOOM DIALOGEX 0, 0, 317, 136
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,69,108,80,12
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,230,18
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,255,18
LTEXT "LiveZoom Toggle:",IDC_STATIC,7,110,62,8
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,230,13
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,230,27
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,230,32
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,249,32
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,255,27
END
RECORD DIALOGEX 0, 0, 260, 181
RECORD DIALOGEX 0, 0, 263, 224
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
@@ -282,19 +284,33 @@ BEGIN
CONTROL "Mono",IDC_MIC_MONO_MIX,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,98,161,30,10
COMBOBOX IDC_MICROPHONE,81,176,152,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_MICROPHONE_LABEL,32,178,47,8
PUSHBUTTON "&Trim",IDC_TRIM_FILE,207,209,53,14
END
SNIP DIALOGEX 0, 0, 260, 68
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
SNIP DIALOGEX 0, 0, 260, 80
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,55,32,80,12
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
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
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
END
PANORAMA DIALOGEX 0, 0, 260, 105
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
END
DEMOTYPE DIALOGEX 0, 0, 260, 249
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_DEMOTYPE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,74,154,80,12
@@ -308,11 +324,11 @@ BEGIN
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
EDITTEXT IDC_DEMOTYPE_FILE,44,137,167,12,ES_AUTOHSCROLL | ES_READONLY
LTEXT "Input file:",IDC_STATIC,7,139,32,8
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,230,24
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,230,24
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,249,24
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,247,24
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,218,11
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,230,16
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,230,16
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,245,16
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,243,16
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,210,8
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,210,8
END
@@ -349,13 +365,13 @@ BEGIN
"OPTIONS", DIALOG
BEGIN
RIGHTMARGIN, 293
BOTTOMMARGIN, 320
BOTTOMMARGIN, 326
END
"ADVANCED_BREAK", DIALOG
BEGIN
RIGHTMARGIN, 207
BOTTOMMARGIN, 215
BOTTOMMARGIN, 214
END
"ZOOM", DIALOG
@@ -383,7 +399,7 @@ BEGIN
BEGIN
LEFTMARGIN, 7
TOPMARGIN, 7
BOTTOMMARGIN, 116
BOTTOMMARGIN, 154
END
1543, DIALOG
@@ -395,22 +411,27 @@ BEGIN
"LIVEZOOM", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 181
TOPMARGIN, 7
BOTTOMMARGIN, 127
BOTTOMMARGIN, 89
END
"RECORD", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 260
TOPMARGIN, 7
BOTTOMMARGIN, 164
BOTTOMMARGIN, 223
END
"SNIP", DIALOG
BEGIN
LEFTMARGIN, 7
TOPMARGIN, 7
BOTTOMMARGIN, 61
END
"PANORAMA", DIALOG
BEGIN
END
"DEMOTYPE", DIALOG
@@ -496,6 +517,16 @@ BEGIN
0
END
ADVANCED_BREAK AFX_DIALOG_LAYOUT
BEGIN
0
END
PANORAMA AFX_DIALOG_LAYOUT
BEGIN
0
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////

View File

@@ -68,8 +68,8 @@
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<DisableSpecificWarnings>4100;4091;4245</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\..\..\;$(MSBuildThisFileDirectory)..\..\..\common\sysinternals;%(AdditionalIncludeDirectories);</AdditionalIncludeDirectories>
<DisableSpecificWarnings>26451;4100;4091;4245</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\..\..\;$(MSBuildThisFileDirectory)..\..\..\common\sysinternals;..\ZoomItBreak;$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Create</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
@@ -90,7 +90,7 @@
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(InterPlatformDir)</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<RandomizedBaseAddress>true</RandomizedBaseAddress>
@@ -109,10 +109,10 @@
<ResourceCompile>
<PreprocessorDefinitions>NDEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x0409</Culture>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<RandomizedBaseAddress>true</RandomizedBaseAddress>
@@ -132,10 +132,10 @@
<ResourceCompile>
<PreprocessorDefinitions>NDEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x0409</Culture>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<FixedBaseAddress>
@@ -156,7 +156,7 @@
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(InterPlatformDir)</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
@@ -174,10 +174,10 @@
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x0409</Culture>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<UACUIAccess>true</UACUIAccess>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
@@ -196,10 +196,10 @@
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x0409</Culture>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<UACUIAccess>true</UACUIAccess>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
@@ -208,6 +208,14 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\ZoomItBreak\BreakTimer.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="AudioSampleGenerator.cpp">
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</MultiProcessorCompilation>
@@ -249,6 +257,14 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="GifRecordingSession.cpp" />
<ClCompile Include="PanoramaCapture.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
</ClCompile>
<ClCompile Include="pch.cpp" />
<ClCompile Include="SelectRectangle.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
@@ -300,11 +316,13 @@
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\ZoomItBreak\BreakTimer.h" />
<ClInclude Include="AudioSampleGenerator.h" />
<ClInclude Include="LoopbackCapture.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
<ClInclude Include="GifRecordingSession.h" />
<ClInclude Include="PanoramaCapture.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Registry.h" />
<ClInclude Include="resource.h" />
@@ -378,4 +396,4 @@
<Import Project="..\..\..\..\packages\robmikh.common.0.0.23-beta\build\native\robmikh.common.targets" Condition="Exists('..\..\..\..\packages\robmikh.common.0.0.23-beta\build\native\robmikh.common.targets')" />
<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.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</Project>
</Project>

View File

@@ -60,6 +60,12 @@
<ClCompile Include="GifRecordingSession.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PanoramaCapture.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\ZoomItBreak\BreakTimer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Registry.h">
@@ -107,6 +113,12 @@
<ClInclude Include="GifRecordingSession.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PanoramaCapture.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\ZoomItBreak\BreakTimer.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Image Include="appicon.ico">

View File

@@ -17,6 +17,8 @@ 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_SnipPanoramaToggleKey = ((HOTKEYF_CONTROL) << 8) | '8';
DWORD g_SnipOcrToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_ALT) << 8) | '6';
DWORD g_ShowExpiredTime = 1;
DWORD g_SliderZoomLevel = 3;
@@ -24,6 +26,7 @@ BOOLEAN g_AnimateZoom = TRUE;
BOOLEAN g_SmoothImage = TRUE;
DWORD g_PenColor = COLOR_RED;
DWORD g_BreakPenColor = COLOR_RED;
DWORD g_BreakBackgroundColor = 0;
DWORD g_RootPenWidth = PEN_WIDTH;
int g_FontScale = 10;
DWORD g_BreakTimeout = 10;
@@ -40,6 +43,7 @@ BOOLEAN g_ShowTrayIcon = TRUE;
BOOLEAN g_SnapToGrid = TRUE;
BOOLEAN g_TelescopeZoomOut = TRUE;
BOOLEAN g_BreakOnSecondary = FALSE;
BOOLEAN g_BreakLockWorkstation = FALSE;
LOGFONT g_LogFont;
BOOLEAN g_DemoTypeUserDriven = false;
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
@@ -66,10 +70,13 @@ 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"SnipPanoramaToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaToggleKey, static_cast<DOUBLE>(g_SnipPanoramaToggleKey) },
{ 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) },
{ L"OptionsShown", SETTING_TYPE_BOOLEAN, 0, &g_OptionsShown, static_cast<DOUBLE>(g_OptionsShown) },
{ L"BreakPenColor", SETTING_TYPE_DWORD, 0, &g_BreakPenColor, static_cast<DOUBLE>(g_BreakPenColor) },
{ L"BreakBackgroundColor", SETTING_TYPE_DWORD, 0, &g_BreakBackgroundColor, static_cast<DOUBLE>(g_BreakBackgroundColor) },
{ L"BreakTimerKey", SETTING_TYPE_DWORD, 0, &g_BreakToggleKey, static_cast<DOUBLE>(g_BreakToggleKey) },
{ L"DemoTypeToggleKey", SETTING_TYPE_DWORD, 0, &g_DemoTypeToggleKey, static_cast<DOUBLE>(g_DemoTypeToggleKey) },
{ L"DemoTypeFile", SETTING_TYPE_STRING, sizeof( g_DemoTypeFile ), g_DemoTypeFile, static_cast<DOUBLE>(0) },
@@ -85,6 +92,7 @@ REG_SETTING RegSettings[] = {
{ L"BreakTimerPosition", SETTING_TYPE_DWORD, 0, &g_BreakTimerPosition, static_cast<DOUBLE>(g_BreakTimerPosition) },
{ L"BreakShowDesktop", SETTING_TYPE_BOOLEAN, 0, &g_BreakShowDesktop, static_cast<DOUBLE>(g_BreakShowDesktop) },
{ L"BreakOnSecondary", SETTING_TYPE_BOOLEAN, 0, &g_BreakOnSecondary,static_cast<DOUBLE>(g_BreakOnSecondary) },
{ L"BreakLockWorkstation", SETTING_TYPE_BOOLEAN, 0, &g_BreakLockWorkstation, static_cast<DOUBLE>(g_BreakLockWorkstation) },
{ L"FontScale", SETTING_TYPE_DWORD, 0, &g_FontScale, static_cast<DOUBLE>(g_FontScale) },
{ L"ShowExpiredTime", SETTING_TYPE_BOOLEAN, 0, &g_ShowExpiredTime, static_cast<DOUBLE>(g_ShowExpiredTime) },
{ L"ShowTrayIcon", SETTING_TYPE_BOOLEAN, 0, &g_ShowTrayIcon, static_cast<DOUBLE>(g_ShowTrayIcon) },

File diff suppressed because it is too large Load Diff

View File

@@ -15,4 +15,14 @@ RCZOOMIT64 BINRES MOVEABLE PURE RCZOOMIT_x64_path
#endif
// Embed the break timer screensaver for the current platform.
// The .scr is built by the ZoomItBreak project into the shared output directory.
#ifdef _M_IX86
RCZOOMITSCR BINRES MOVEABLE PURE "ZoomItBreak.scr"
#elif defined(_M_X64)
RCZOOMITSCR BINRES MOVEABLE PURE "ZoomItBreak64.scr"
#elif defined(_M_ARM64)
RCZOOMITSCR BINRES MOVEABLE PURE "ZoomItBreak64a.scr"
#endif
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "ZoomIt.exe.manifest"

View File

@@ -53,6 +53,9 @@
#include <winrt/Windows.Storage.Pickers.h>
#include <winrt/Windows.Storage.FileProperties.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <winrt/Windows.Media.Ocr.h>
#include <Windows.Graphics.Imaging.Interop.h>
#include <filesystem>

View File

@@ -78,6 +78,8 @@
#define IDC_RECORD_FRAME_RATE2 1059
#define IDC_RECORD_SCALING 1059
#define IDC_SNIP_HOTKEY 1060
#define IDC_SNIP_OCR_HOTKEY 1112
#define IDC_SNIP_PANORAMA_HOTKEY 1114
#define IDC_CAPTURE_AUDIO 1061
#define IDC_MICROPHONE 1062
#define IDC_PEN_CONTROL 1063
@@ -111,12 +113,15 @@
#define IDC_SMOOTH_IMAGE 1107
#define IDC_CAPTURE_SYSTEM_AUDIO 1108
#define IDC_MICROPHONE_LABEL 1109
#define IDC_MIC_MONO_MIX 1110
#define IDC_TRIM_FILE 1110
#define IDC_MIC_MONO_MIX 1111
#define IDC_CHECK_LOCK_WORKSTATION 1112
#define IDC_SAVE 40002
#define IDC_COPY 40004
#define IDC_RECORD 40006
#define IDC_RECORD_HOTKEY 40007
#define IDC_COPY_CROP 40008
#define IDC_COPY_OCR 40014
#define IDC_SAVE_CROP 40009
#define IDC_DEMOTYPE_HOTKEY 40011
@@ -125,8 +130,8 @@
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 120
#define _APS_NEXT_COMMAND_VALUE 40013
#define _APS_NEXT_CONTROL_VALUE 1099
#define _APS_NEXT_COMMAND_VALUE 40015
#define _APS_NEXT_CONTROL_VALUE 1113
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -0,0 +1,520 @@
//============================================================================
//
// BreakTimer.cpp
//
// Shared break timer rendering module used by both ZoomIt and the
// ZoomItBreak screensaver (.scr).
//
// Copyright (C) Mark Russinovich
// Sysinternals - www.sysinternals.com
//
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//============================================================================
// When built inside ZoomIt (with PCH), pch.h is included automatically.
// When built for the screensaver project, we include the headers we need.
#ifndef __ZOOMIT_SCREENSAVER__
#include "pch.h"
#endif
#include "BreakTimer.h"
#include <stdio.h>
#pragma comment(lib, "gdiplus.lib")
#pragma comment(lib, "Msimg32.lib")
#pragma comment(lib, "Winmm.lib")
//----------------------------------------------------------------------------
//
// BreakTimer_UpdateMonitorInfo
//
// Determine monitor geometry for the given screen point.
//
//----------------------------------------------------------------------------
void BreakTimer_UpdateMonitorInfo( POINT point, MONITORINFO* monInfo )
{
HMONITOR hMon = MonitorFromPoint( point, MONITOR_DEFAULTTONEAREST );
if( hMon != nullptr )
{
monInfo->cbSize = sizeof *monInfo;
GetMonitorInfo( hMon, monInfo );
}
else
{
*monInfo = {};
HDC hdcScreen = CreateDC( L"DISPLAY", nullptr, nullptr, nullptr );
if( hdcScreen != nullptr )
{
monInfo->rcMonitor.right = GetDeviceCaps( hdcScreen, HORZRES );
monInfo->rcMonitor.bottom = GetDeviceCaps( hdcScreen, VERTRES );
DeleteDC( hdcScreen );
}
}
}
//----------------------------------------------------------------------------
//
// BreakTimer_LoadImageFile
//
// Use GDI+ to load an image file and return an HBITMAP.
//
//----------------------------------------------------------------------------
HBITMAP BreakTimer_LoadImageFile( PTCHAR Filename )
{
HBITMAP hBmp;
Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromFile( Filename );
if( bitmap == nullptr || bitmap->GetHBITMAP( NULL, &hBmp ) != Gdiplus::Ok )
{
delete bitmap;
return NULL;
}
delete bitmap;
return hBmp;
}
//----------------------------------------------------------------------------
//
// BreakTimer_CreateFadedDesktopBackground
//
// Creates a snapshot of the desktop that is faded and alpha-blended
// with black.
//
//----------------------------------------------------------------------------
HBITMAP BreakTimer_CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop )
{
int width = rcScreen->right - rcScreen->left;
int height = rcScreen->bottom - rcScreen->top;
HDC hdcScreen = hdc;
HDC hdcMem = CreateCompatibleDC( hdcScreen );
HBITMAP hBitmap = CreateCompatibleBitmap( hdcScreen, width, height );
HBITMAP hOld = static_cast<HBITMAP>( SelectObject( hdcMem, hBitmap ) );
HBRUSH hBrush = CreateSolidBrush( RGB( 0, 0, 0 ) );
// Start with black background.
FillRect( hdcMem, rcScreen, hBrush );
if( rcCrop != NULL && rcCrop->left != -1 )
{
// Copy screen contents that are not cropped.
BitBlt( hdcMem, rcCrop->left, rcCrop->top,
rcCrop->right - rcCrop->left,
rcCrop->bottom - rcCrop->top,
hdcScreen, rcCrop->left, rcCrop->top, SRCCOPY );
}
// Blend screen contents into the black background.
BLENDFUNCTION blend = { 0 };
blend.BlendOp = AC_SRC_OVER;
blend.BlendFlags = 0;
blend.SourceConstantAlpha = 0x4F;
blend.AlphaFormat = 0;
AlphaBlend( hdcMem, 0, 0, width, height,
hdcScreen, rcScreen->left, rcScreen->top,
width, height, blend );
SelectObject( hdcMem, hOld );
DeleteDC( hdcMem );
DeleteObject( hBrush );
return hBitmap;
}
//----------------------------------------------------------------------------
//
// BreakTimer_Init
//
// Create fonts, backing bitmap, and optionally load background.
// Returns TRUE on success.
//
//----------------------------------------------------------------------------
BOOLEAN BreakTimer_Init(
HWND hWnd,
BreakTimerState* state,
const BreakTimerSettings* settings,
int timeoutSeconds,
HBITMAP hExistingBackground,
HDC hExistingBackgroundDC )
{
state->active = TRUE;
state->timeoutSeconds = timeoutSeconds;
// Get screen DC.
state->hdcScreen = CreateDC( L"DISPLAY", static_cast<PTCHAR>( NULL ),
static_cast<PTCHAR>( NULL ),
static_cast<CONST DEVMODE*>( NULL ) );
if( !state->hdcScreen )
return FALSE;
// Determine monitor.
POINT cursorPos;
GetCursorPos( &cursorPos );
BreakTimer_UpdateMonitorInfo( cursorPos, &state->monInfo );
state->width = state->monInfo.rcMonitor.right - state->monInfo.rcMonitor.left;
state->height = state->monInfo.rcMonitor.bottom - state->monInfo.rcMonitor.top;
// Manage background bitmap.
if( hExistingBackground )
{
// Caller supplied a pre-captured background (e.g. from command line).
state->hBackgroundBmp = hExistingBackground;
state->hDcBackgroundFile = hExistingBackgroundDC;
}
else if( settings->showBackgroundFile && !settings->showDesktop )
{
// Load image file.
state->hBackgroundBmp = BreakTimer_LoadImageFile(
const_cast<PTCHAR>( settings->backgroundFile ) );
if( !state->hBackgroundBmp )
return FALSE;
state->hDcBackgroundFile = CreateCompatibleDC( state->hdcScreen );
SelectObject( state->hDcBackgroundFile, state->hBackgroundBmp );
}
else if( settings->showBackgroundFile && settings->showDesktop )
{
// Faded desktop screenshot.
HDC hDcDesktop = GetDC( NULL );
state->hBackgroundBmp = BreakTimer_CreateFadedDesktopBackground(
hDcDesktop, &state->monInfo.rcMonitor, NULL );
ReleaseDC( NULL, hDcDesktop );
state->hDcBackgroundFile = CreateCompatibleDC( state->hdcScreen );
SelectObject( state->hDcBackgroundFile, state->hBackgroundBmp );
}
else
{
state->hBackgroundBmp = NULL;
state->hDcBackgroundFile = NULL;
}
// Create fonts.
LOGFONT lf = settings->logFont;
lf.lfHeight = state->height / 5;
state->hTimerFont = CreateFontIndirect( &lf );
lf.lfHeight = state->height / 8;
state->hNegativeTimerFont = CreateFontIndirect( &lf );
// Create backing bitmap for double buffering.
state->hdcScreenCompat = CreateCompatibleDC( state->hdcScreen );
state->bmp.bmBitsPixel = static_cast<BYTE>( GetDeviceCaps( state->hdcScreen, BITSPIXEL ) );
state->bmp.bmPlanes = static_cast<BYTE>( GetDeviceCaps( state->hdcScreen, PLANES ) );
state->bmp.bmWidth = state->width;
state->bmp.bmHeight = state->height;
state->bmp.bmWidthBytes = ( ( state->bmp.bmWidth + 15 ) & ~15 ) / 8;
state->hbmpCompat = CreateBitmap( state->bmp.bmWidth, state->bmp.bmHeight,
state->bmp.bmPlanes, state->bmp.bmBitsPixel, static_cast<CONST VOID*>( NULL ) );
SelectObject( state->hdcScreenCompat, state->hbmpCompat );
SetTextColor( state->hdcScreenCompat, settings->penColor );
SetBkMode( state->hdcScreenCompat, TRANSPARENT );
SelectObject( state->hdcScreenCompat, state->hTimerFont );
return TRUE;
}
//----------------------------------------------------------------------------
//
// BreakTimer_Tick
//
// Decrement counter, invalidate window, play sound at zero.
//
//----------------------------------------------------------------------------
void BreakTimer_Tick(
HWND hWnd,
BreakTimerState* state,
const BreakTimerSettings* settings )
{
state->timeoutSeconds -= 1;
InvalidateRect( hWnd, NULL, FALSE );
if( state->timeoutSeconds == 0 && settings->playSound )
{
PlaySound( settings->soundFile, NULL, SND_FILENAME | SND_ASYNC );
}
}
//----------------------------------------------------------------------------
//
// BreakTimer_Paint
//
// Render the break timer into the back buffer and blit to the paint DC.
//
//----------------------------------------------------------------------------
void BreakTimer_Paint(
HDC hdc,
BreakTimerState* state,
const BreakTimerSettings* settings )
{
RECT rc, rc1;
TCHAR timerText[16];
TCHAR negativeTimerText[16];
// Fill background (white by default, black if backgroundColor == 1).
rc.top = rc.left = 0;
rc.bottom = state->height;
rc.right = state->width;
if( settings->backgroundColor )
{
HBRUSH hBrush = CreateSolidBrush( RGB( 0, 0, 0 ) );
FillRect( state->hdcScreenCompat, &rc, hBrush );
DeleteObject( hBrush );
}
else
{
FillRect( state->hdcScreenCompat, &rc, GetSysColorBrush( COLOR_WINDOW ) );
}
// Draw background bitmap if present.
if( state->hBackgroundBmp )
{
BITMAP local_bmp;
GetObject( state->hBackgroundBmp, sizeof( local_bmp ), &local_bmp );
SetStretchBltMode( state->hdcScreenCompat,
settings->smoothImage ? HALFTONE : COLORONCOLOR );
if( settings->backgroundStretch )
{
StretchBlt( state->hdcScreenCompat, 0, 0, state->width, state->height,
state->hDcBackgroundFile, 0, 0,
local_bmp.bmWidth, local_bmp.bmHeight, SRCCOPY | CAPTUREBLT );
}
else
{
BitBlt( state->hdcScreenCompat,
state->width / 2 - local_bmp.bmWidth / 2,
state->height / 2 - local_bmp.bmHeight / 2,
local_bmp.bmWidth, local_bmp.bmHeight,
state->hDcBackgroundFile, 0, 0, SRCCOPY | CAPTUREBLT );
}
}
// Format timer text.
if( state->timeoutSeconds > 0 )
{
_stprintf( timerText, L"% 2d:%02d",
state->timeoutSeconds / 60, state->timeoutSeconds % 60 );
}
else
{
_tcscpy( timerText, L"0:00" );
}
// Measure timer text.
rc.left = rc.top = 0;
DrawText( state->hdcScreenCompat, timerText, -1, &rc,
DT_NOCLIP | DT_LEFT | DT_NOPREFIX | DT_CALCRECT );
// Measure expired text if needed.
rc1.left = rc1.right = rc1.bottom = rc1.top = 0;
if( settings->showExpiredTime && state->timeoutSeconds < 0 )
{
_stprintf( negativeTimerText, L"(-% 2d:%02d)",
-state->timeoutSeconds / 60, -state->timeoutSeconds % 60 );
HFONT prevFont = static_cast<HFONT>(
SelectObject( state->hdcScreenCompat, state->hNegativeTimerFont ) );
DrawText( state->hdcScreenCompat, negativeTimerText, -1, &rc1,
DT_NOCLIP | DT_LEFT | DT_NOPREFIX | DT_CALCRECT );
SelectObject( state->hdcScreenCompat, prevFont );
}
// Position vertically.
switch( settings->timerPosition )
{
case 0: case 1: case 2:
rc.top = 50;
break;
case 3: case 4: case 5:
rc.top = ( state->height - ( rc.bottom - rc.top ) ) / 2;
break;
case 6: case 7: case 8:
rc.top = state->height - rc.bottom - 50 - rc1.bottom;
break;
}
// Position horizontally.
switch( settings->timerPosition )
{
case 0: case 3: case 6:
rc.left = 50;
break;
case 1: case 4: case 7:
rc.left = ( state->width - ( rc.right - rc.left ) ) / 2;
break;
case 2: case 5: case 8:
rc.left = state->width - rc.right - 50;
break;
}
rc.bottom += rc.top;
rc.right += rc.left;
// Draw timer text.
DrawText( state->hdcScreenCompat, timerText, -1, &rc,
DT_NOCLIP | DT_LEFT | DT_NOPREFIX );
// Draw expired text below the timer.
if( settings->showExpiredTime && state->timeoutSeconds < 0 )
{
rc1.top = rc.bottom + 10;
rc1.left = rc.left + ( ( rc.right - rc.left ) - ( rc1.right - rc1.left ) ) / 2;
HFONT prevFont = static_cast<HFONT>(
SelectObject( state->hdcScreenCompat, state->hNegativeTimerFont ) );
DrawText( state->hdcScreenCompat, negativeTimerText, -1, &rc1,
DT_NOCLIP | DT_LEFT | DT_NOPREFIX );
SelectObject( state->hdcScreenCompat, prevFont );
}
// Copy to screen.
BitBlt( hdc, 0, 0, state->width, state->height,
state->hdcScreenCompat, 0, 0, SRCCOPY | CAPTUREBLT );
}
//----------------------------------------------------------------------------
//
// BreakTimer_Cleanup
//
// Free the GDI resources used by the break timer.
//
//----------------------------------------------------------------------------
void BreakTimer_Cleanup(
BreakTimerState* state,
BOOLEAN freeBackground )
{
if( freeBackground && state->hBackgroundBmp )
{
DeleteObject( state->hBackgroundBmp );
DeleteDC( state->hDcBackgroundFile );
state->hBackgroundBmp = NULL;
state->hDcBackgroundFile = NULL;
}
if( state->hTimerFont )
{
DeleteObject( state->hTimerFont );
state->hTimerFont = NULL;
}
if( state->hNegativeTimerFont )
{
DeleteObject( state->hNegativeTimerFont );
state->hNegativeTimerFont = NULL;
}
if( state->hdcScreen )
{
DeleteDC( state->hdcScreen );
state->hdcScreen = NULL;
}
if( state->hdcScreenCompat )
{
DeleteDC( state->hdcScreenCompat );
state->hdcScreenCompat = NULL;
}
if( state->hbmpCompat )
{
DeleteObject( state->hbmpCompat );
state->hbmpCompat = NULL;
}
state->active = FALSE;
}
//----------------------------------------------------------------------------
//
// BreakTimer_AdjustTime
//
// Round to the nearest minute boundary and adjust by deltaMinutes.
// Resets the 1-second timer on the window.
//
//----------------------------------------------------------------------------
void BreakTimer_AdjustTime(
HWND hWnd,
BreakTimerState* state,
int deltaMinutes )
{
int breakTimeout = state->timeoutSeconds;
if( deltaMinutes > 0 )
{
if( breakTimeout < 0 ) breakTimeout = 0;
if( breakTimeout % 60 )
{
breakTimeout += ( 60 - breakTimeout % 60 );
deltaMinutes--;
}
breakTimeout += deltaMinutes * 60;
}
else
{
int absDelta = -deltaMinutes;
if( breakTimeout % 60 )
{
breakTimeout -= breakTimeout % 60;
absDelta--;
}
breakTimeout -= absDelta * 60;
}
if( breakTimeout < 0 ) breakTimeout = 0;
state->timeoutSeconds = breakTimeout;
KillTimer( hWnd, 0 );
SetTimer( hWnd, 0, 1000, NULL );
InvalidateRect( hWnd, NULL, TRUE );
}
//----------------------------------------------------------------------------
//
// BreakScrConfig_GetPath
//
// Build the full path to the config file in %TEMP%.
//
//----------------------------------------------------------------------------
static void BreakScrConfig_GetPath( TCHAR* path, size_t cch )
{
GetTempPath( static_cast<DWORD>( cch ), path );
_tcscat( path, BREAKSCR_CONFIG_FILE );
}
//----------------------------------------------------------------------------
//
// BreakScrConfig_Write
//
//----------------------------------------------------------------------------
BOOLEAN BreakScrConfig_Write( const BreakScrConfig* config )
{
TCHAR path[MAX_PATH];
BreakScrConfig_GetPath( path, MAX_PATH );
HANDLE hFile = CreateFile( path, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile == INVALID_HANDLE_VALUE )
return FALSE;
DWORD written;
BOOL ok = WriteFile( hFile, config, sizeof( *config ), &written, NULL );
CloseHandle( hFile );
return ok && written == sizeof( *config );
}
//----------------------------------------------------------------------------
//
// BreakScrConfig_Read
//
//----------------------------------------------------------------------------
BOOLEAN BreakScrConfig_Read( BreakScrConfig* config )
{
TCHAR path[MAX_PATH];
BreakScrConfig_GetPath( path, MAX_PATH );
HANDLE hFile = CreateFile( path, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile == INVALID_HANDLE_VALUE )
return FALSE;
DWORD bytesRead;
BOOL ok = ReadFile( hFile, config, sizeof( *config ), &bytesRead, NULL );
CloseHandle( hFile );
if( !ok || bytesRead != sizeof( *config ) )
return FALSE;
if( config->magic != BREAKSCR_CONFIG_MAGIC )
return FALSE;
return TRUE;
}

View File

@@ -0,0 +1,141 @@
//============================================================================
//
// BreakTimer.h
//
// Shared break timer rendering module used by both ZoomIt and the
// ZoomItBreak screensaver (.scr).
//
// Copyright (C) Mark Russinovich
// Sysinternals - www.sysinternals.com
//
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//============================================================================
#pragma once
#include <windows.h>
#include <tchar.h>
#define GDIPVER 0x0110
#include <gdiplus.h>
//----------------------------------------------------------------------------
// BreakTimerSettings — read-only configuration, populated from globals
// or from command-line arguments in the screensaver.
//----------------------------------------------------------------------------
struct BreakTimerSettings
{
DWORD penColor;
DWORD backgroundColor; // 0 = white, 1 = black
DWORD timerPosition; // 08 (3×3 grid)
DWORD opacity; // 0100
DWORD showExpiredTime; // 0 or 1
BOOLEAN smoothImage;
BOOLEAN backgroundStretch;
BOOLEAN playSound;
TCHAR soundFile[MAX_PATH];
BOOLEAN showDesktop;
BOOLEAN showBackgroundFile;
TCHAR backgroundFile[MAX_PATH];
LOGFONT logFont;
};
//----------------------------------------------------------------------------
// BreakTimerState — runtime state for an active break timer.
//----------------------------------------------------------------------------
struct BreakTimerState
{
BOOLEAN active;
int timeoutSeconds; // counts down; goes negative if expired
HFONT hTimerFont;
HFONT hNegativeTimerFont;
HBITMAP hBackgroundBmp;
HDC hDcBackgroundFile;
HDC hdcScreen;
HDC hdcScreenCompat;
HBITMAP hbmpCompat;
BITMAP bmp;
int width;
int height;
MONITORINFO monInfo;
};
//----------------------------------------------------------------------------
// Shared utility functions
//----------------------------------------------------------------------------
// Determine monitor geometry for the given screen point.
void BreakTimer_UpdateMonitorInfo( POINT point, MONITORINFO* monInfo );
// Load an image file via GDI+; returns an HBITMAP or NULL on failure.
HBITMAP BreakTimer_LoadImageFile( PTCHAR Filename );
// Capture a faded (alpha-blended with black) screenshot of the desktop.
HBITMAP BreakTimer_CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop );
//----------------------------------------------------------------------------
// Break timer lifecycle
//----------------------------------------------------------------------------
// Create fonts, backing bitmap, and load background.
// The caller is responsible for creating/showing the window itself.
// |timeoutSeconds| is already in seconds (e.g. g_BreakTimeout * 60 + 1).
BOOLEAN BreakTimer_Init(
HWND hWnd,
BreakTimerState* state,
const BreakTimerSettings* settings,
int timeoutSeconds,
HBITMAP hExistingBackground, // optional pre-captured background
HDC hExistingBackgroundDC // optional DC for above
);
// Called every second; decrements the counter and invalidates the window.
void BreakTimer_Tick(
HWND hWnd,
BreakTimerState* state,
const BreakTimerSettings* settings
);
// Render the timer into hdcScreenCompat then BitBlt to hdc (from BeginPaint).
void BreakTimer_Paint(
HDC hdc,
BreakTimerState* state,
const BreakTimerSettings* settings
);
// Free fonts, DCs, bitmaps. If |freeBackground| is false the background
// bitmap/DC are left for the caller to manage (e.g. shallow destroy).
void BreakTimer_Cleanup(
BreakTimerState* state,
BOOLEAN freeBackground
);
// Adjust the remaining time by |deltaMinutes| (positive = add time).
// Resets the 1-second timer on hWnd.
void BreakTimer_AdjustTime(
HWND hWnd,
BreakTimerState* state,
int deltaMinutes
);
//----------------------------------------------------------------------------
// BreakScrConfig — binary blob written to a temp file by ZoomIt and
// read by the screensaver on startup. This avoids command-line arg
// issues since Windows launches screensavers with only /s.
//----------------------------------------------------------------------------
#define BREAKSCR_CONFIG_MAGIC 0x5A4D4253 // 'ZMBS'
#define BREAKSCR_CONFIG_FILE L"ZoomItBreakConfig.dat"
struct BreakScrConfig
{
DWORD magic; // must be BREAKSCR_CONFIG_MAGIC
int timeoutSeconds;
BOOL resumed; // set TRUE by screensaver on first launch
BreakTimerSettings settings;
TCHAR screenshotPath[MAX_PATH];
};
// Write config to %TEMP%\BREAKSCR_CONFIG_FILE.
BOOLEAN BreakScrConfig_Write( const BreakScrConfig* config );
// Read config from %TEMP%\BREAKSCR_CONFIG_FILE.
BOOLEAN BreakScrConfig_Read( BreakScrConfig* config );

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@@ -0,0 +1,27 @@
//============================================================================
//
// ZoomItBreak.rc
//
// Minimal resources required by Scrnsavw.lib.
//
//============================================================================
#include <windows.h>
#include <scrnsave.h>
// Embed DPI-awareness manifest so the screensaver sees native resolution.
// This ensures the pre-captured desktop screenshot (saved at physical pixels
// by the DPI-aware ZoomIt process) matches the screensaver window dimensions.
1 RT_MANIFEST "ZoomItBreak.manifest"
// IDS_DESCRIPTION is used by scrnsavw.lib as the window class name.
STRINGTABLE
BEGIN
IDS_DESCRIPTION, "ZoomIt Break Timer"
END
// Stub configuration dialog - never shown (ScreenSaverConfigureDialog returns FALSE).
DLG_SCRNSAVECONFIGURE DIALOG 0, 0, 200, 60
STYLE WS_DLGFRAME | WS_POPUP | WS_VISIBLE | DS_MODALFRAME | WS_CAPTION
CAPTION "ZoomIt Break Timer"
BEGIN
END

View File

@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>18.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{94ba3051-c8d7-454a-9d46-1a7c78e228a3}</ProjectGuid>
<RootNamespace>ZoomItBreak</RootNamespace>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<!--
Output .scr instead of .exe. A screensaver is a renamed executable.
Use a separate intermediate dir to avoid colliding with ZoomIt.
-->
<PropertyGroup>
<TargetExt>.scr</TargetExt>
<GenerateManifest>false</GenerateManifest>
<IntDir>$(Platform)\$(Configuration)\ZoomItBreak\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
<TargetName>$(ProjectName)64</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
<TargetName>$(ProjectName)64a</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
<TargetName>$(ProjectName)64</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
<TargetName>$(ProjectName)64a</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<DisableSpecificWarnings>4100;4091;4245</DisableSpecificWarnings>
<AdditionalIncludeDirectories>..\..\..\;$(MSBuildThisFileDirectory)..\..\..\common\sysinternals;..\ZoomIt;%(AdditionalIncludeDirectories);</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="BreakTimer.cpp" />
<ClCompile Include="ZoomItBreakScr.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="ZoomItBreak.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="BreakTimer.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</Project>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="BreakTimer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ZoomItBreakScr.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="BreakTimer.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="ZoomItBreak.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,283 @@
//============================================================================
//
// ZoomItBreakScr.cpp
//
// ZoomIt break timer screensaver (.scr). When launched by Winlogon on the
// Screen-saver desktop with password protection, the user must authenticate
// to dismiss it. The break timer countdown and rendering are provided by
// the shared BreakTimer module.
//
// Copyright (C) Mark Russinovich
// Sysinternals - www.sysinternals.com
//
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//============================================================================
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <scrnsave.h>
#define GDIPVER 0x0110
#include <gdiplus.h>
#include "BreakTimer.h"
static void DbgPrint( LPCTSTR fmt, ... )
{
TCHAR buf[512];
va_list ap;
#pragma warning( push )
#pragma warning( disable : 26492 )
va_start( ap, fmt );
#pragma warning( pop )
_vsntprintf( buf, _countof(buf), fmt, ap );
va_end( ap );
buf[_countof(buf)-1] = 0;
OutputDebugString( buf );
}
#pragma comment(lib, "scrnsavw.lib")
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "gdiplus.lib")
#pragma comment(lib, "Msimg32.lib")
#pragma comment(lib, "Winmm.lib")
//----------------------------------------------------------------------------
// Globals
//----------------------------------------------------------------------------
static BreakTimerSettings g_Settings;
static BreakTimerState g_State;
static ULONG_PTR g_GdiplusToken;
static TCHAR g_ScreenshotPath[MAX_PATH] = { 0 };
static int g_LastSavedTimeout = 0; // For state persistence
//----------------------------------------------------------------------------
// Load settings from the binary config file written by ZoomIt,
// falling back to hard-coded defaults if the file is missing.
//----------------------------------------------------------------------------
static void LoadSettings( void )
{
BreakScrConfig config;
if( BreakScrConfig_Read( &config ) )
{
g_Settings = config.settings;
g_State.timeoutSeconds = config.timeoutSeconds;
_tcscpy( g_ScreenshotPath, config.screenshotPath );
DbgPrint( L"[BreakScr] Config loaded: timeout=%d, bgFile=%d, showDesktop=%d, screenshot=%s\n",
config.timeoutSeconds, config.settings.showBackgroundFile,
config.settings.showDesktop, config.screenshotPath );
return;
}
DbgPrint( L"[BreakScr] Config file not found, using fallback defaults\n" );
// Fallback defaults (for testing the .scr directly).
memset( &g_Settings, 0, sizeof( g_Settings ) );
g_Settings.penColor = RGB( 255, 0, 0 );
g_Settings.timerPosition = 4;
g_Settings.opacity = 100;
g_Settings.showExpiredTime = 1;
g_Settings.smoothImage = TRUE;
g_Settings.backgroundStretch = FALSE;
g_Settings.showDesktop = TRUE;
g_Settings.showBackgroundFile = FALSE;
g_State.timeoutSeconds = 600;
NONCLIENTMETRICS ncm = { sizeof( ncm ) };
SystemParametersInfo( SPI_GETNONCLIENTMETRICS, sizeof( ncm ), &ncm, 0 );
g_Settings.logFont = ncm.lfMessageFont;
}
//----------------------------------------------------------------------------
//
// ScreenSaverProc
//
// Main window procedure for the screensaver, called by Scrnsavw.lib.
//
//----------------------------------------------------------------------------
LRESULT WINAPI ScreenSaverProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
case WM_CREATE:
{
DbgPrint( L"[BreakScr] WM_CREATE: hwnd=%p\n", hWnd );
// Initialize GDI+.
Gdiplus::GdiplusStartupInput startupIn;
Gdiplus::GdiplusStartup( &g_GdiplusToken, &startupIn, NULL );
LoadSettings();
// Check if a previous screensaver instance already ran (resumed == TRUE).
// On first launch, ZoomIt sets resumed = FALSE, so we skip the deduction.
BreakScrConfig resumeConfig;
if( BreakScrConfig_Read( &resumeConfig ) && resumeConfig.resumed )
{
// Subtract the screensaver idle timeout to compensate for
// the time the screensaver wasn't running on the lock screen.
UINT scrTimeout = 0;
SystemParametersInfo( SPI_GETSCREENSAVETIMEOUT, 0, &scrTimeout, 0 );
g_State.timeoutSeconds -= static_cast<int>( scrTimeout );
if( g_State.timeoutSeconds < 0 && !g_Settings.showExpiredTime )
g_State.timeoutSeconds = 0;
DbgPrint( L"[BreakScr] Resumption: subtracted %u sec idle, timeout=%d\n",
scrTimeout, g_State.timeoutSeconds );
}
// Mark as resumed so subsequent screensaver launches know to deduct idle time.
{
BreakScrConfig markConfig;
if( BreakScrConfig_Read( &markConfig ) )
{
markConfig.resumed = TRUE;
BreakScrConfig_Write( &markConfig );
}
}
// Load pre-captured screenshot if provided.
HBITMAP hBgBmp = NULL;
HDC hBgDC = NULL;
if( g_ScreenshotPath[0] )
{
hBgBmp = BreakTimer_LoadImageFile( g_ScreenshotPath );
DbgPrint( L"[BreakScr] LoadImageFile(%s) => %p\n", g_ScreenshotPath, hBgBmp );
if( hBgBmp )
{
HDC hdcScreen = CreateDC( L"DISPLAY", NULL, NULL, NULL );
hBgDC = CreateCompatibleDC( hdcScreen );
SelectObject( hBgDC, hBgBmp );
DeleteDC( hdcScreen );
}
}
int timeout = g_State.timeoutSeconds;
memset( &g_State, 0, sizeof( g_State ) );
DbgPrint( L"[BreakScr] Calling BreakTimer_Init, timeout=%d\n", timeout );
if( !BreakTimer_Init( hWnd, &g_State, &g_Settings, timeout, hBgBmp, hBgDC ) )
{
DbgPrint( L"[BreakScr] BreakTimer_Init FAILED\n" );
PostMessage( hWnd, WM_CLOSE, 0, 0 );
return 0;
}
DbgPrint( L"[BreakScr] BreakTimer_Init OK, active=%d\n", g_State.active );
// Prevent the monitor from blanking due to power management.
SetThreadExecutionState( ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED );
// Kick off the first tick and start the 1-second timer.
SendMessage( hWnd, WM_TIMER, 1, 0 );
SetTimer( hWnd, 1, 1000, NULL );
return 0;
}
case WM_TIMER:
if( wParam == 1 )
{
BreakTimer_Tick( hWnd, &g_State, &g_Settings );
// Periodically save state (every 5 seconds) for resumption after
// credential provider timeout. This allows the screensaver to continue
// from where it left off if a student triggers the login screen but
// doesn't authenticate.
if( g_State.timeoutSeconds != g_LastSavedTimeout &&
g_State.timeoutSeconds % 5 == 0 )
{
BreakScrConfig config;
if( BreakScrConfig_Read( &config ) )
{
config.timeoutSeconds = g_State.timeoutSeconds;
if( BreakScrConfig_Write( &config ) )
{
g_LastSavedTimeout = g_State.timeoutSeconds;
DbgPrint( L"[BreakScr] Saved state: %d seconds remaining\n",
g_State.timeoutSeconds );
}
}
}
}
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint( hWnd, &ps );
if( g_State.active )
{
BreakTimer_Paint( hdc, &g_State, &g_Settings );
}
EndPaint( hWnd, &ps );
return 0;
}
case WM_DESTROY:
DbgPrint( L"[BreakScr] WM_DESTROY\n" );
SetThreadExecutionState( ES_CONTINUOUS ); // Restore default power behavior
KillTimer( hWnd, 1 );
BreakTimer_Cleanup( &g_State, TRUE );
Gdiplus::GdiplusShutdown( g_GdiplusToken );
return 0;
//------------------------------------------------------------------
// Prevent DefScreenSaverProc from auto-closing on user input.
// The screensaver must stay up until the break timer expires or
// the user authenticates via Ctrl+Alt+Del. DefScreenSaverProc
// would close the window on mouse movement, clicks, keyboard,
// or deactivation.
//------------------------------------------------------------------
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_KEYDOWN:
if( wParam == 'W' || wParam == 'K' )
{
g_Settings.backgroundColor = ( wParam == 'K' ) ? 1 : 0;
InvalidateRect( hWnd, NULL, FALSE );
}
return 0;
case WM_KEYUP:
case WM_SYSKEYDOWN:
return 0;
case WM_ACTIVATE:
case WM_ACTIVATEAPP:
// Don't close on deactivation (e.g. LockWorkStation switches desktop).
return 0;
case WM_SYSCOMMAND:
// Block SC_CLOSE from Alt+F4 etc.
if( ( wParam & 0xFFF0 ) == SC_CLOSE )
return 0;
break;
}
return DefScreenSaverProc( hWnd, msg, wParam, lParam );
}
//----------------------------------------------------------------------------
//
// ScreenSaverConfigureDialog
//
// No configuration — ZoomIt handles all settings.
//
//----------------------------------------------------------------------------
BOOL WINAPI ScreenSaverConfigureDialog( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
return FALSE;
}
//----------------------------------------------------------------------------
//
// RegisterDialogClasses
//
// Nothing to register.
//
//----------------------------------------------------------------------------
BOOL WINAPI RegisterDialogClasses( HANDLE hInst )
{
return TRUE;
}

View File

@@ -91,3 +91,12 @@ void Trace::ZoomItActivateSnip() noexcept
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::ZoomItActivateSnipOcr() noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"ZoomIt_ActivateSnipOcr",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@@ -14,4 +14,5 @@ public:
static void ZoomItActivateDemoType() noexcept;
static void ZoomItActivateRecord() noexcept;
static void ZoomItActivateSnip() noexcept;
static void ZoomItActivateSnipOcr() noexcept;
};

View File

@@ -70,6 +70,8 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
{ L"DrawToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"RecordToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"SnipToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"SnipOcrToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"SnipPanoramaToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"BreakTimerKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"DemoTypeToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
{ L"PenColor", SPECIAL_SEMANTICS_COLOR },

View File

@@ -76,7 +76,7 @@ bool isExcluded(HWND window)
}
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu}),
SettingsObserver({ SettingId::FrameEnabled, SettingId::Hotkey, SettingId::IncreaseOpacityHotkey, SettingId::DecreaseOpacityHotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu }),
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
m_useCentralizedLLKH(useLLKH),
m_mainThreadId(mainThreadId),
@@ -150,6 +150,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
switch (id)
{
case SettingId::Hotkey:
case SettingId::IncreaseOpacityHotkey:
case SettingId::DecreaseOpacityHotkey:
{
RegisterHotkey();
}
@@ -360,10 +362,8 @@ void AlwaysOnTop::RegisterHotkey() const
// Register pin hotkey
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), settings->hotkey.get_modifiers(), settings->hotkey.get_code());
// Register transparency hotkeys using the same modifiers as the pin hotkey
UINT modifiers = settings->hotkey.get_modifiers();
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), settings->increaseOpacityHotkey.get_modifiers(), settings->increaseOpacityHotkey.get_code());
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), settings->decreaseOpacityHotkey.get_modifiers(), settings->decreaseOpacityHotkey.get_code());
}
void AlwaysOnTop::RegisterLLKH()

View File

@@ -13,6 +13,8 @@ namespace NonLocalizable
const static wchar_t* SettingsFileName = L"settings.json";
const static wchar_t* HotkeyID = L"hotkey";
const static wchar_t* IncreaseOpacityHotkeyID = L"increase-opacity-hotkey";
const static wchar_t* DecreaseOpacityHotkeyID = L"decrease-opacity-hotkey";
const static wchar_t* SoundEnabledID = L"sound-enabled";
const static wchar_t* ShowInSystemMenuID = L"show-in-system-menu";
const static wchar_t* FrameEnabledID = L"frame-enabled";
@@ -100,16 +102,21 @@ void AlwaysOnTopSettings::LoadSettings()
const auto currentSettings = AlwaysOnTopSettings::settings();
auto updatedSettings = std::make_shared<Settings>(*currentSettings);
std::vector<SettingId> changedSettings;
if (const auto jsonVal = values.get_json(NonLocalizable::HotkeyID))
{
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
if (updatedSettings->hotkey.get_modifiers() != val.get_modifiers() || updatedSettings->hotkey.get_key() != val.get_key() || updatedSettings->hotkey.get_code() != val.get_code())
const auto updateHotkeySetting = [&](const wchar_t* hotkeyName, auto& currentHotkey, SettingId settingId) {
if (const auto jsonVal = values.get_json(hotkeyName))
{
updatedSettings->hotkey = val;
changedSettings.push_back(SettingId::Hotkey);
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
if (currentHotkey.get_modifiers() != val.get_modifiers() || currentHotkey.get_key() != val.get_key() || currentHotkey.get_code() != val.get_code())
{
currentHotkey = val;
changedSettings.push_back(settingId);
}
}
}
};
updateHotkeySetting(NonLocalizable::HotkeyID, updatedSettings->hotkey, SettingId::Hotkey);
updateHotkeySetting(NonLocalizable::IncreaseOpacityHotkeyID, updatedSettings->increaseOpacityHotkey, SettingId::IncreaseOpacityHotkey);
updateHotkeySetting(NonLocalizable::DecreaseOpacityHotkeyID, updatedSettings->decreaseOpacityHotkey, SettingId::DecreaseOpacityHotkey);
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
{

View File

@@ -18,6 +18,8 @@ class SettingsObserver;
struct Settings
{
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
PowerToysSettings::HotkeyObject increaseOpacityHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, VK_OEM_PLUS); // win + ctrl + '+'
PowerToysSettings::HotkeyObject decreaseOpacityHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, VK_OEM_MINUS); // win + ctrl + '-'
static constexpr int minTransparencyPercentage = 20; // minimum transparency (can't go below 20%)
static constexpr int maxTransparencyPercentage = 100; // maximum (fully opaque)
static constexpr int transparencyStep = 10; // step size for +/- adjustment

View File

@@ -3,6 +3,8 @@
enum class SettingId
{
Hotkey = 0,
IncreaseOpacityHotkey,
DecreaseOpacityHotkey,
SoundEnabled,
ShowInSystemMenu,
FrameEnabled,

View File

@@ -16,6 +16,8 @@
namespace NonLocalizable
{
const wchar_t ModulePath[] = L"PowerToys.AlwaysOnTop.exe";
// Keep in sync with src\modules\alwaysontop\AlwaysOnTop\AlwaysOnTop.cpp
const wchar_t PinnedWindowProp[] = L"AlwaysOnTop_Pinned";
}
namespace
@@ -27,6 +29,8 @@ namespace
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_HOTKEY[] = L"hotkey";
const wchar_t JSON_KEY_INCREASE_OPACITY_HOTKEY[] = L"increase-opacity-hotkey";
const wchar_t JSON_KEY_DECREASE_OPACITY_HOTKEY[] = L"decrease-opacity-hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value";
}
@@ -107,27 +111,38 @@ public:
virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
if (!m_enabled)
{
return false;
}
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
if (hotkeyId == 0)
{
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
if (!is_process_running())
{
Enable();
}
if (hotkeyId == 0)
SetEvent(m_hPinEvent);
return true;
}
if (hotkeyId == 1 || hotkeyId == 2)
{
const HWND foregroundWindow = GetForegroundWindow();
if (!foregroundWindow || !IsWindow(foregroundWindow) || !GetPropW(foregroundWindow, NonLocalizable::PinnedWindowProp))
{
SetEvent(m_hPinEvent);
}
else if (hotkeyId == 1)
{
SetEvent(m_hIncreaseOpacityEvent);
}
else if (hotkeyId == 2)
{
SetEvent(m_hDecreaseOpacityEvent);
return false;
}
if (!is_process_running())
{
Enable();
}
SetEvent(hotkeyId == 1 ? m_hIncreaseOpacityEvent : m_hDecreaseOpacityEvent);
return true;
}
@@ -136,48 +151,48 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
size_t count = 0;
// Hotkey 0: Pin/Unpin (e.g., Win+Ctrl+T)
if (m_hotkey.key)
constexpr size_t hotkeyCount = 3;
Hotkey configuredHotkeys[hotkeyCount] = { m_hotkey, m_increaseOpacityHotkey, m_decreaseOpacityHotkey };
for (size_t i = 0; i < hotkeyCount; ++i)
{
if (hotkeys && buffer_size > count)
{
hotkeys[count] = m_hotkey;
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}",
m_hotkey.win, m_hotkey.ctrl, m_hotkey.shift, m_hotkey.alt, m_hotkey.key);
}
count++;
configuredHotkeys[i].id = static_cast<int>(i);
configuredHotkeys[i].isShown = configuredHotkeys[i].key != 0;
}
// Hotkey 1: Increase opacity (same modifiers + VK_OEM_PLUS '=')
if (m_hotkey.key)
if (hotkeys)
{
if (hotkeys && buffer_size > count)
const size_t countToCopy = (buffer_size < hotkeyCount) ? buffer_size : hotkeyCount;
for (size_t i = 0; i < countToCopy; ++i)
{
hotkeys[count] = m_hotkey;
hotkeys[count].key = VK_OEM_PLUS; // '=' key
Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
hotkeys[i] = configuredHotkeys[i];
}
count++;
}
// Hotkey 2: Decrease opacity (same modifiers + VK_OEM_MINUS '-')
if (m_hotkey.key)
{
if (hotkeys && buffer_size > count)
{
hotkeys[count] = m_hotkey;
hotkeys[count].key = VK_OEM_MINUS; // '-' key
Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
}
count++;
}
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
configuredHotkeys[0].win,
configuredHotkeys[0].ctrl,
configuredHotkeys[0].shift,
configuredHotkeys[0].alt,
configuredHotkeys[0].key,
configuredHotkeys[0].isShown);
Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
configuredHotkeys[1].win,
configuredHotkeys[1].ctrl,
configuredHotkeys[1].shift,
configuredHotkeys[1].alt,
configuredHotkeys[1].key,
configuredHotkeys[1].isShown);
Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
configuredHotkeys[2].win,
configuredHotkeys[2].ctrl,
configuredHotkeys[2].shift,
configuredHotkeys[2].alt,
configuredHotkeys[2].key,
configuredHotkeys[2].isShown);
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", count);
return count;
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", hotkeyCount);
return hotkeyCount;
}
// Enable the powertoy
@@ -279,21 +294,34 @@ private:
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
const auto parseSingleHotkey = [](const winrt::Windows::Data::Json::JsonObject& propertiesObject, const wchar_t* hotkeyName, Hotkey& hotkey) {
try
{
auto jsonHotkeyObject = propertiesObject.GetNamedObject(hotkeyName).GetNamedObject(JSON_KEY_VALUE);
hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
}
};
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
parseSingleHotkey(propertiesObject, JSON_KEY_HOTKEY, m_hotkey);
parseSingleHotkey(propertiesObject, JSON_KEY_INCREASE_OPACITY_HOTKEY, m_increaseOpacityHotkey);
parseSingleHotkey(propertiesObject, JSON_KEY_DECREASE_OPACITY_HOTKEY, m_decreaseOpacityHotkey);
}
catch (...)
{
Logger::error("Failed to initialize AlwaysOnTop start shortcut");
Logger::error("Failed to initialize AlwaysOnTop shortcuts");
}
}
else
@@ -329,7 +357,9 @@ private:
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
Hotkey m_hotkey;
Hotkey m_hotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = 'T' };
Hotkey m_increaseOpacityHotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = VK_OEM_PLUS };
Hotkey m_decreaseOpacityHotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = VK_OEM_MINUS };
// Handle to event used to pin/unpin windows
HANDLE m_hPinEvent;

View File

@@ -213,22 +213,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
BuildAndInitMoreCommands();
if (!string.IsNullOrEmpty(model.Command?.Name))
{
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
};
// Only set the icon on the context item for us if our command didn't
// have its own icon
UpdateDefaultContextItemIcon();
}
TryCreateDefaultCommandContextItem(model);
lock (_moreCommandsLock)
{
@@ -238,6 +223,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Initialized |= InitializedState.SelectionInitialized;
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
UpdateProperty(nameof(IsSelectedInitialized));
}
@@ -335,9 +321,16 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
_itemTitle = model.Title;
_defaultCommandContextItemViewModel?.Command = Command;
_defaultCommandContextItemViewModel?.UpdateTitle(_itemTitle);
UpdateDefaultContextItemIcon();
if (_defaultCommandContextItemViewModel is not null)
{
_defaultCommandContextItemViewModel.Command = Command;
_defaultCommandContextItemViewModel.UpdateTitle(_itemTitle);
UpdateDefaultContextItemIcon();
}
else
{
TryCreateDefaultCommandContextItem(model);
}
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
@@ -403,7 +396,15 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_titleCache.Invalidate();
UpdateProperty(nameof(Title), nameof(Name));
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
if (_defaultCommandContextItemViewModel is not null)
{
_defaultCommandContextItemViewModel.UpdateTitle(model.Command.Name);
}
else
{
TryCreateDefaultCommandContextItem(model);
}
break;
case nameof(Command.Icon):
@@ -413,6 +414,46 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
}
/// <summary>
/// Creates <see cref="_defaultCommandContextItemViewModel"/> when it does not exist
/// yet and the current command has a non-empty name. This covers the case
/// where an extension initially exposes a <c>NoOpCommand</c> (empty name)
/// and later switches to a concrete command after <see cref="SlowInitializeProperties"/> has already run.
/// When a new instance is created, the snapshot is refreshed and
/// <see cref="AllCommands"/> is notified.
/// </summary>
private void TryCreateDefaultCommandContextItem(ICommandItem model)
{
if (_defaultCommandContextItemViewModel is not null)
{
return;
}
if (string.IsNullOrEmpty(model.Command?.Name))
{
return;
}
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
};
UpdateDefaultContextItemIcon();
lock (_moreCommandsLock)
{
RefreshMoreCommandStateUnsafe();
}
UpdateProperty(nameof(AllCommands));
}
private void UpdateDefaultContextItemIcon() =>
// Command icon takes precedence over our icon on the primary command

View File

@@ -301,8 +301,19 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
var dockSettings = settings.DockSettings;
var allPinnedCommands = dockSettings.AllPinnedCommands;
var pinnedBandsForThisProvider = allPinnedCommands.Where(c => c.ProviderId == ProviderId);
// Track which command IDs we've already added to avoid duplicates
// from settings that were pinned multiple times.
HashSet<string> seenCommandIds = new(bands.Select(b => b.Id));
foreach (var (providerId, commandId) in pinnedBandsForThisProvider)
{
if (!seenCommandIds.Add(commandId))
{
Logger.LogWarning($"Skipping duplicate pinned dock band command {commandId} for provider {providerId}");
continue;
}
Logger.LogDebug($"Looking for pinned dock band command {commandId} for provider {providerId}");
// First, try to lookup the command as one of this provider's
@@ -436,16 +447,42 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
}
}
public void PinDockBand(string commandId, IServiceProvider serviceProvider)
public void PinDockBand(string commandId, IServiceProvider serviceProvider, Dock.DockPinSide side = Dock.DockPinSide.Start, bool? showTitles = null, bool? showSubtitles = null)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var dockSettings = settings.DockSettings;
// Prevent duplicate pins — check all sections
if (dockSettings.StartBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId) ||
dockSettings.CenterBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId) ||
dockSettings.EndBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId))
{
Logger.LogDebug($"Dock band '{commandId}' from provider '{this.ProviderId}' is already pinned; skipping.");
return;
}
var bandSettings = new DockBandSettings
{
CommandId = commandId,
ProviderId = this.ProviderId,
ShowTitles = showTitles,
ShowSubtitles = showSubtitles,
};
settings.DockSettings.StartBands.Add(bandSettings);
switch (side)
{
case Dock.DockPinSide.Center:
settings.DockSettings.CenterBands.Add(bandSettings);
break;
case Dock.DockPinSide.End:
settings.DockSettings.EndBands.Add(bandSettings);
break;
case Dock.DockPinSide.Start:
default:
settings.DockSettings.StartBands.Add(bandSettings);
break;
}
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));

View File

@@ -365,9 +365,9 @@ public sealed partial class MainListPage : DynamicListPage,
}
// prefilter fallbacks
var globalFallbacks = _settingsService.Settings.GetGlobalFallbacks();
var specialFallbacks = new List<TopLevelViewModel>(globalFallbacks.Length);
var commonFallbacks = new List<TopLevelViewModel>(commands.Count - globalFallbacks.Length);
var configuredGlobalFallbackIds = _settingsService.Settings.GetGlobalFallbacks();
var specialFallbacks = new List<TopLevelViewModel>(configuredGlobalFallbackIds.Length);
var commonFallbacks = new List<TopLevelViewModel>(Math.Max(commands.Count - configuredGlobalFallbackIds.Length, 0));
foreach (var s in commands)
{
@@ -376,7 +376,7 @@ public sealed partial class MainListPage : DynamicListPage,
continue;
}
if (globalFallbacks.Contains(s.Id))
if (configuredGlobalFallbackIds.Contains(s.Id))
{
specialFallbacks.Add(s);
}
@@ -509,7 +509,7 @@ public sealed partial class MainListPage : DynamicListPage,
return;
}
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && configuredGlobalFallbackIds.Contains(s.Id));
_scoredFallbackItems = InternalListHelpers.FilterListWithScores(newFallbacksForScoring, searchQuery, _scoringFunction);
if (token.IsCancellationRequested)

View File

@@ -21,6 +21,7 @@ public sealed partial class DockViewModel
private readonly IContextMenuFactory _contextMenuFactory;
private DockSettings _settings;
private bool _isEditing;
public TaskScheduler Scheduler { get; }
@@ -52,6 +53,12 @@ public sealed partial class DockViewModel
private void DockBands_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (_isEditing)
{
Logger.LogDebug("Skipping DockBands_CollectionChanged during edit mode");
return;
}
Logger.LogDebug("Starting DockBands_CollectionChanged");
SetupBands();
Logger.LogDebug("Ended DockBands_CollectionChanged");
@@ -302,7 +309,12 @@ public sealed partial class DockViewModel
_snapshotCenterBands = null;
_snapshotEndBands = null;
_snapshotBandViewModels = null;
_settingsService.Save();
// Save without hotReload to avoid triggering SettingsChanged → SetupBands,
// which could race with stale DockBands_CollectionChanged work items and
// re-add bands that were just unpinned.
_settingsService.Save(hotReload: false);
_isEditing = false;
Logger.LogDebug("Saved band order to settings");
}
@@ -317,6 +329,8 @@ public sealed partial class DockViewModel
/// </summary>
public void SnapshotBandOrder()
{
_isEditing = true;
var dockSettings = _settingsService.Settings.DockSettings;
_snapshotStartBands = dockSettings.StartBands.Select(b => b.Clone()).ToList();
_snapshotCenterBands = dockSettings.CenterBands.Select(b => b.Clone()).ToList();
@@ -391,6 +405,7 @@ public sealed partial class DockViewModel
_snapshotCenterBands = null;
_snapshotEndBands = null;
_snapshotBandViewModels = null;
_isEditing = false;
Logger.LogDebug("Restored band order from snapshot");
}

View File

@@ -214,6 +214,7 @@ public partial class ListItemViewModel : CommandItemViewModel
if (addedCommand)
{
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
}
}
}
@@ -252,6 +253,7 @@ public partial class ListItemViewModel : CommandItemViewModel
oldCommand?.SafeCleanup();
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
}
}

View File

@@ -2,6 +2,14 @@
// 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.UI.ViewModels.Dock;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record PinToDockMessage(string ProviderId, string CommandId, bool Pin);
public record PinToDockMessage(
string ProviderId,
string CommandId,
bool Pin,
DockPinSide Side = DockPinSide.Start,
bool? ShowTitles = null,
bool? ShowSubtitles = null);

View File

@@ -0,0 +1,15 @@
// 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.UI.ViewModels.Settings;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record ShowPinToDockDialogMessage(
string ProviderId,
string CommandId,
string Title,
string Subtitle,
IconInfoViewModel? Icon,
DockSide DockSide);

View File

@@ -261,8 +261,20 @@ public partial class ShellViewModel : ObservableObject,
return;
}
// Determine whether this is the root/home page navigation BEFORE
// computing providerContext. When navigating back to the root page we
// must use an empty provider context so that home-page list items don't
// inherit a pinning-capable context left over from the previous sub-page
// (which can happen e.g. when the window is hidden while on a sub-page).
// isMainPage must be evaluated here; if it were moved inside the
// "if (command is IPage)" block below, it would be too late to affect
// the providerContext that is passed to the new page view-model.
var isMainPage = command == _rootPage;
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext);
var providerContext = isMainPage
? CommandProviderContext.Empty
: _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext);
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
@@ -272,7 +284,6 @@ public partial class ShellViewModel : ObservableObject,
{
CoreLogger.LogDebug($"Navigating to page");
var isMainPage = command == _rootPage;
_isNested = !isMainPage;
_currentlyTransient = message.TransientPage;

View File

@@ -707,7 +707,7 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
{
if (message.Pin)
{
wrapper?.PinDockBand(message.CommandId, _serviceProvider);
wrapper?.PinDockBand(message.CommandId, _serviceProvider, message.Side, message.ShowTitles, message.ShowSubtitles);
}
else
{

View File

@@ -4,6 +4,8 @@
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
@@ -56,8 +58,32 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
List<IContextItem> moreCommands = [];
var itemId = commandItem.Command.Id;
var providerContext = page.ProviderContext;
// AppListItems can be surfaced on the main page even though they still
// belong to the All Apps provider.
// MainListPage only returns our in-proc wrappers/items.
if (providerContext.ProviderId != AllAppsCommandProvider.WellKnownId &&
page is ListViewModel { IsMainPage: true } &&
commandItem.Model.Unsafe is AppListItem)
{
providerContext = _topLevelCommandManager.LookupProvider(AllAppsCommandProvider.WellKnownId)?.GetProviderContext() ?? providerContext;
}
var supportsPinning = providerContext.SupportsPinning;
// Also bail early for ListItemViewModels that wrap a TopLevelViewModel.
// For those items, TopLevelViewModel.BuildContextMenu() already includes
// the correct pin commands by calling AddMoreCommandsToTopLevel with the
// item's own provider context. Adding them again here (using the page's
// potentially incorrect provider context) would produce duplicate pin
// entries such as two "Pin to Dock" buttons.
// Check SupportsPinning first to avoid the .Unsafe type-check in the
// common non-pinning case.
if (supportsPinning && commandItem.Model.Unsafe is TopLevelViewModel)
{
return results;
}
if (supportsPinning &&
!string.IsNullOrEmpty(itemId))
{
@@ -185,7 +211,8 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
pin: !alreadyPinned,
PinLocation.Dock,
_settingsService,
_topLevelCommandManager);
_topLevelCommandManager,
commandItemViewModel: commandItem);
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
moreCommands.Add(contextItem);
@@ -236,6 +263,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly bool _pin;
private readonly PinLocation _pinLocation;
private readonly CommandItemViewModel? _commandItemViewModel;
private bool IsPinToDock => _pinLocation == PinLocation.Dock;
@@ -253,7 +281,8 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
bool pin,
PinLocation pinLocation,
ISettingsService settingsService,
TopLevelCommandManager topLevelCommandManager)
TopLevelCommandManager topLevelCommandManager,
CommandItemViewModel? commandItemViewModel = null)
{
_commandId = commandId;
_providerId = providerId;
@@ -261,6 +290,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
_settingsService = settingsService;
_topLevelCommandManager = topLevelCommandManager;
_pin = pin;
_commandItemViewModel = commandItemViewModel;
}
public override CommandResult Invoke()
@@ -312,7 +342,11 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
private void PinToDock()
{
PinToDockMessage message = new(_providerId, _commandId, true);
var title = _commandItemViewModel?.Title ?? string.Empty;
var subtitle = _commandItemViewModel?.Subtitle ?? string.Empty;
var icon = _commandItemViewModel?.Icon;
var dockSide = _settingsService.Settings.DockSettings.Side;
ShowPinToDockDialogMessage message = new(_providerId, _commandId, title, subtitle, icon, dockSide);
WeakReferenceMessenger.Default.Send(message);
}

View File

@@ -189,7 +189,7 @@
<VisualStateGroup x:Name="OrientationStates">
<VisualState x:Name="HorizontalState">
<VisualState.Setters>
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Disabled" />
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
@@ -213,7 +213,7 @@
<VisualState.Setters>
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Disabled" />
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
@@ -228,7 +228,7 @@
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
<Setter Target="ScrollForwardIcon.Glyph" Value="&#xE72A;" />
<Setter Target="ScrollForwardIcon.Glyph" Value="&#xEDDC;" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -67,15 +67,52 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
_viewModel = viewModel;
InitializeComponent();
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
ViewModel.CenterItems.CollectionChanged += CenterItems_CollectionChanged;
Loaded += DockControl_Loaded;
Unloaded += DockControl_Unloaded;
// Start with edit mode disabled - normal click behavior
UpdateEditMode(false);
}
private void DockControl_Loaded(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.UnregisterAll(this);
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
ViewModel.CenterItems.CollectionChanged -= CenterItems_CollectionChanged;
ViewModel.CenterItems.CollectionChanged += CenterItems_CollectionChanged;
UpdateEditModeTeachingTip();
}
private void DockControl_Unloaded(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.UnregisterAll(this);
ViewModel.CenterItems.CollectionChanged -= CenterItems_CollectionChanged;
if (EditButtonsTeachingTip.IsOpen)
{
EditButtonsTeachingTip.IsOpen = false;
}
if (ContextMenuFlyout.IsOpen)
{
ContextMenuFlyout.Hide();
}
if (AddBandFlyout.IsOpen)
{
AddBandFlyout.Hide();
}
if (EditModeContextMenu.IsOpen)
{
EditModeContextMenu.Hide();
}
}
private void CenterItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateCenterVisibility();
@@ -125,7 +162,38 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
};
}
EditButtonsTeachingTip.IsOpen = isEditMode;
UpdateEditModeTeachingTip();
}
private void UpdateEditModeTeachingTip()
{
if (XamlRoot is null || ContentGrid.XamlRoot is null || EditButtonsTeachingTip.Parent is null)
{
return;
}
if (!IsEditMode)
{
if (EditButtonsTeachingTip.IsOpen)
{
EditButtonsTeachingTip.IsOpen = false;
}
return;
}
if (!EditButtonsTeachingTip.IsOpen)
{
EditButtonsTeachingTip.IsOpen = true;
}
}
private static void PreparePopupForShow(FlyoutBase popup, FrameworkElement placementTarget)
{
if (placementTarget.XamlRoot is not null && popup.XamlRoot != placementTarget.XamlRoot)
{
popup.XamlRoot = placementTarget.XamlRoot;
}
}
internal void EnterEditMode()
@@ -214,6 +282,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
ShowTitlesMenuItem.IsChecked = _editModeContextBand.ShowTitles;
ShowSubtitlesMenuItem.IsChecked = _editModeContextBand.ShowSubtitles;
PreparePopupForShow(EditModeContextMenu, dockItem);
EditModeContextMenu.ShowAt(
dockItem,
new FlyoutShowOptions()
@@ -232,6 +301,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = true;
PreparePopupForShow(ContextMenuFlyout, dockItem);
ContextMenuFlyout.ShowAt(
dockItem,
new FlyoutShowOptions()
@@ -320,6 +390,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = false;
PreparePopupForShow(ContextMenuFlyout, RootGrid);
ContextMenuFlyout.ShowAt(
this.RootGrid,
new FlyoutShowOptions()
@@ -516,6 +587,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
AddBandListView.Visibility = hasAvailableBands ? Visibility.Visible : Visibility.Collapsed;
// Show the flyout
PreparePopupForShow(AddBandFlyout, button);
AddBandFlyout.ShowAt(button);
}
}

View File

@@ -35,10 +35,7 @@ public sealed partial class DockItemControl : Control
{
if (d is DockItemControl control)
{
// Collapse the tooltip when the string is null or empty so an
// empty tooltip bubble doesn't appear on hover.
var text = e.NewValue as string;
ToolTipService.SetToolTip(control, string.IsNullOrEmpty(text) ? null : text);
control.UpdateToolTip();
}
}
@@ -91,6 +88,7 @@ public sealed partial class DockItemControl : Control
private FrameworkElement? _iconPresenter;
private DockControl? _parentDock;
private ToolTip? _toolTip;
private long _dockSideCallbackToken = -1;
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -184,9 +182,33 @@ public sealed partial class DockItemControl : Control
{
UpdateTextVisibility();
UpdateIconVisibility();
UpdateToolTip();
UpdateAlignment();
}
private void UpdateToolTip()
{
var text = ToolTip;
if (string.IsNullOrEmpty(text))
{
ToolTipService.SetToolTip(this, null);
_toolTip = null;
return;
}
// Wait until the control is connected to a XamlRoot before creating
// the tooltip popup; dock items are materialized very early in startup.
if (XamlRoot is null)
{
return;
}
_toolTip ??= new ToolTip();
_toolTip.Content = text;
_toolTip.XamlRoot = XamlRoot;
ToolTipService.SetToolTip(this, _toolTip);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
@@ -232,6 +254,8 @@ public sealed partial class DockItemControl : Control
DockControl.DockSideProperty,
OnParentDockSideChanged);
}
UpdateToolTip();
}
private void DockItemControl_ActualThemeChanged(FrameworkElement sender, object args)
@@ -250,6 +274,9 @@ public sealed partial class DockItemControl : Control
_dockSideCallbackToken = -1;
_parentDock = null;
}
ToolTipService.SetToolTip(this, null);
_toolTip = null;
}
private void OnParentDockSideChanged(DependencyObject sender, DependencyProperty dp)

View File

@@ -4,7 +4,6 @@
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Windows.Win32;
using WinUIEx;
namespace Microsoft.CmdPal.UI.Dock;
@@ -32,16 +31,6 @@ internal static class DockSettingsToViews
};
}
public static Microsoft.UI.Xaml.Media.SystemBackdrop? GetSystemBackdrop(DockBackdrop backdrop)
{
return backdrop switch
{
DockBackdrop.Transparent => new TransparentTintBackdrop(),
DockBackdrop.Acrylic => null,
_ => throw new NotImplementedException(),
};
}
public static uint GetAppBarEdge(DockSide side)
{
return side switch

View File

@@ -5,7 +5,6 @@
using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -27,7 +26,6 @@ using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
using WinRT.Interop;
using WinUIEx;
using WindowExtensions = Microsoft.CmdPal.UI.Helpers.WindowExtensions;
namespace Microsoft.CmdPal.UI.Dock;
@@ -59,7 +57,10 @@ public sealed partial class DockWindow : WindowEx,
private DockControl _dock;
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource;
private bool _isUpdatingBackdrop;
private BackdropParameters? _lastAppliedAcrylicBackdrop;
private DockSize _lastSize;
private bool _isDisposed;
// Store the original WndProc
private WNDPROC? _originalWndProc;
@@ -78,6 +79,7 @@ public sealed partial class DockWindow : WindowEx,
viewModel = serviceProvider.GetService<DockViewModel>()!;
_themeService = serviceProvider.GetRequiredService<IThemeService>();
_themeService.ThemeChanged += ThemeService_ThemeChanged;
InitializeBackdropSupport();
_windowViewModel = new DockWindowViewModel(_themeService);
_dock = new DockControl(viewModel);
@@ -156,14 +158,7 @@ public sealed partial class DockWindow : WindowEx,
private void UpdateSettingsOnUiThread()
{
this.viewModel.UpdateSettings(_settings);
SystemBackdrop = DockSettingsToViews.GetSystemBackdrop(_settings.Backdrop);
// If the backdrop is acrylic, things are more complicated
if (_settings.Backdrop == DockBackdrop.Acrylic)
{
SetAcrylic();
}
UpdateBackdrop();
_dock.UpdateSettings(_settings);
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
@@ -183,31 +178,98 @@ public sealed partial class DockWindow : WindowEx,
CreateAppBar(_hwnd);
}
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
// other Shell surfaces are using, this cannot be set in XAML however.
private void SetAcrylic()
private void InitializeBackdropSupport()
{
if (DesktopAcrylicController.IsSupported())
{
// Hooking up the policy object.
_configurationSource = new SystemBackdropConfiguration
{
// Initial configuration state.
IsInputActive = true,
};
UpdateAcrylic();
}
}
private void UpdateAcrylic()
private void UpdateBackdrop()
{
if (_acrylicController != null)
// Prevent re-entrance when backdrop changes trigger theme refresh work.
if (_isUpdatingBackdrop)
{
return;
}
_isUpdatingBackdrop = true;
try
{
switch (_settings.Backdrop)
{
case DockBackdrop.Transparent:
if (SystemBackdrop is not TransparentTintBackdrop)
{
CleanupBackdropControllers();
SetupTransparentBackdrop();
}
break;
case DockBackdrop.Acrylic:
default:
SetupDesktopAcrylic(_themeService.CurrentDockTheme.BackdropParameters);
break;
}
}
catch (Exception ex)
{
Logger.LogError("Failed to update dock backdrop", ex);
}
finally
{
_isUpdatingBackdrop = false;
}
}
private void SetupTransparentBackdrop()
{
if (SystemBackdrop is not TransparentTintBackdrop)
{
SystemBackdrop = new TransparentTintBackdrop();
}
_lastAppliedAcrylicBackdrop = null;
}
private void CleanupBackdropControllers()
{
if (_acrylicController is not null)
{
_acrylicController.RemoveAllSystemBackdropTargets();
_acrylicController.Dispose();
_acrylicController = null;
}
var backdrop = _themeService.CurrentDockTheme.BackdropParameters;
_lastAppliedAcrylicBackdrop = null;
}
private void SetupDesktopAcrylic(BackdropParameters backdrop)
{
var needsAcrylicUpdate = _acrylicController is null || _lastAppliedAcrylicBackdrop != backdrop;
if (!needsAcrylicUpdate)
{
return;
}
CleanupBackdropControllers();
// Fall back to the transparent backdrop if acrylic is not supported.
if (_configurationSource is null || !DesktopAcrylicController.IsSupported())
{
SetupTransparentBackdrop();
return;
}
// DesktopAcrylicController and SystemBackdrop can't be active simultaneously.
SystemBackdrop = null;
_acrylicController = new DesktopAcrylicController
{
Kind = DesktopAcrylicKind.Thin,
@@ -221,29 +283,20 @@ public sealed partial class DockWindow : WindowEx,
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
_lastAppliedAcrylicBackdrop = backdrop;
}
private void DisposeAcrylic()
{
if (_acrylicController is not null)
{
_acrylicController.Dispose();
_acrylicController = null!;
_configurationSource = null!;
}
CleanupBackdropControllers();
_configurationSource = null;
}
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
{
DispatcherQueue.TryEnqueue(() =>
{
// We only need to handle acrylic here.
// Transparent background is handled directly in XAML by binding to
// the DockWindowViewModel's ColorizationColor properties.
if (_settings.Backdrop == DockBackdrop.Acrylic)
{
UpdateAcrylic();
}
UpdateBackdrop();
// ActualTheme / RequestedTheme sync,
// as pilfered from WindowThemeSynchronizer
@@ -617,18 +670,38 @@ public sealed partial class DockWindow : WindowEx,
public void Dispose()
{
DisposeAcrylic();
_windowViewModel.Dispose();
Cleanup();
GC.SuppressFinalize(this);
}
private void DockWindow_Closed(object sender, WindowEventArgs args)
{
_settingsService.SettingsChanged -= SettingsChangedHandler;
Dispose();
}
private void Cleanup()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
_settingsService?.SettingsChanged -= SettingsChangedHandler;
Activated -= DockWindow_Activated;
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
WeakReferenceMessenger.Default.UnregisterAll(this);
DisposeAcrylic();
_windowViewModel.Dispose();
// Remove our app bar registration
DestroyAppBar(_hwnd);
if (_appBarData.hWnd != IntPtr.Zero)
{
DestroyAppBar(_hwnd);
}
// Unhook the window procedure
ShowDesktop.RemoveHook();

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Dock.PinToDockDialogContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Top dock: horizontal bar at top (left/center/right) -->
<x:String x:Key="TopStartPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM24 18C25.1046 18 26 18.8954 26 20C26 21.1046 25.1046 22 24 22H12C10.8954 22 10 21.1046 10 20C10 18.8954 10.8954 18 12 18H24Z</x:String>
<x:String x:Key="TopCenterPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM38 18C39.1046 18 40 18.8954 40 20C40 21.1046 39.1046 22 38 22H26C24.8954 22 24 21.1046 24 20C24 18.8954 24.8954 18 26 18H38Z</x:String>
<x:String x:Key="TopEndPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM52 18C53.1046 18 54 18.8954 54 20C54 21.1046 53.1046 22 52 22H40C38.8954 22 38 21.1046 38 20C38 18.8954 38.8954 18 40 18H52Z</x:String>
<!-- Bottom dock: horizontal bar at bottom (left/center/right) -->
<x:String x:Key="BottomStartPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM24 42C25.1046 42 26 42.8954 26 44C26 45.1046 25.1046 46 24 46H12C10.8954 46 10 45.1046 10 44C10 42.8954 10.8954 42 12 42H24Z</x:String>
<x:String x:Key="BottomCenterPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM38 42C39.1046 42 40 42.8954 40 44C40 45.1046 39.1046 46 38 46H26C24.8954 46 24 45.1046 24 44C24 42.8954 24.8954 42 26 42H38Z</x:String>
<x:String x:Key="BottomEndPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM52 42C53.1046 42 54 42.8954 54 44C54 45.1046 53.1046 46 52 46H40C38.8954 46 38 45.1046 38 44C38 42.8954 38.8954 42 40 42H52Z</x:String>
<!-- Left dock: vertical bar at left (top/center/bottom) -->
<x:String x:Key="LeftStartPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM12 14C13.1046 14 14 14.8954 14 16V28C14 29.1046 13.1046 30 12 30C10.8954 30 10 29.1046 10 28V16C10 14.8954 10.8954 14 12 14Z</x:String>
<x:String x:Key="LeftCenterPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM12 24C13.1046 24 14 24.8954 14 26V38C14 39.1046 13.1046 40 12 40C10.8954 40 10 39.1046 10 38V26C10 24.8954 10.8954 24 12 24Z</x:String>
<x:String x:Key="LeftEndPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM12 34C13.1046 34 14 34.8954 14 36V48C14 49.1046 13.1046 50 12 50C10.8954 50 10 49.1046 10 48V36C10 34.8954 10.8954 34 12 34Z</x:String>
<!-- Right dock: vertical bar at right (top/center/bottom) -->
<x:String x:Key="RightStartPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM52 14C53.1046 14 54 14.8954 54 16V28C54 29.1046 53.1046 30 52 30C50.8954 30 50 29.1046 50 28V16C50 14.8954 50.8954 14 52 14Z</x:String>
<x:String x:Key="RightCenterPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM52 24C53.1046 24 54 24.8954 54 26V38C54 39.1046 53.1046 40 52 40C50.8954 40 50 39.1046 50 38V26C50 24.8954 50.8954 24 52 24Z</x:String>
<x:String x:Key="RightEndPath">M52.25 8C53.8125 8 55.3021 8.3125 56.7188 8.9375C58.1354 9.5625 59.3854 10.4167 60.4688 11.5C61.5521 12.5833 62.4062 13.8333 63.0312 15.25C63.6562 16.6667 63.9792 18.1667 64 19.75V44.25C64 45.8125 63.6875 47.3021 63.0625 48.7188C62.4375 50.1354 61.5833 51.3854 60.5 52.4688C59.4167 53.5521 58.1667 54.4062 56.75 55.0312C55.3333 55.6562 53.8333 55.9792 52.25 56H11.75C10.1875 56 8.69792 55.6875 7.28125 55.0625C5.86458 54.4375 4.61458 53.5833 3.53125 52.5C2.44792 51.4167 1.59375 50.1667 0.96875 48.75C0.34375 47.3333 0.0208333 45.8333 0 44.25V19.75C0 18.1875 0.3125 16.6979 0.9375 15.2812C1.5625 13.8646 2.41667 12.6146 3.5 11.5312C4.58333 10.4479 5.83333 9.59375 7.25 8.96875C8.66667 8.34375 10.1667 8.02083 11.75 8H52.25ZM11.8438 12C10.8021 12 9.8125 12.2083 8.875 12.625C7.9375 13.0417 7.10417 13.6146 6.375 14.3438C5.64583 15.0729 5.07292 15.9062 4.65625 16.8438C4.23958 17.7812 4.02083 18.7812 4 19.8438V44.1562C4 45.1979 4.20833 46.1875 4.625 47.125C5.04167 48.0625 5.61458 48.8958 6.34375 49.625C7.07292 50.3542 7.90625 50.9271 8.84375 51.3438C9.78125 51.7604 10.7812 51.9792 11.8438 52H52.1562C53.1979 52 54.1875 51.7917 55.125 51.375C56.0625 50.9583 56.8958 50.3854 57.625 49.6562C58.3542 48.9271 58.9271 48.0938 59.3438 47.1562C59.7604 46.2188 59.9792 45.2188 60 44.1562V19.8438C60 18.8021 59.7917 17.8125 59.375 16.875C58.9583 15.9375 58.3854 15.1042 57.6562 14.375C56.9271 13.6458 56.0938 13.0729 55.1562 12.6562C54.2188 12.2396 53.2188 12.0208 52.1562 12H11.8438ZM52 34C53.1046 34 54 34.8954 54 36V48C54 49.1046 53.1046 50 52 50C50.8954 50 50 49.1046 50 48V36C50 34.8954 50.8954 34 52 34Z</x:String>
</ResourceDictionary>
</UserControl.Resources>
<ScrollViewer>
<StackPanel Width="420" Spacing="16">
<!-- Preview -->
<StackPanel Spacing="8">
<Border
Padding="12,8"
HorizontalAlignment="Left"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid ColumnSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Icon -->
<cpcontrols:IconBox
x:Name="PreviewIcon"
Width="16"
Height="16"
VerticalAlignment="Center"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
<!-- Text -->
<StackPanel
x:Name="PreviewTextPanel"
Grid.Column="1"
Margin="8,0,0,0"
VerticalAlignment="Center">
<TextBlock
x:Name="PreviewTitleText"
FontSize="12"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="PreviewSubtitleText"
FontSize="10"
Foreground="{ThemeResource TextFillColorTertiary}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
<Rectangle
Height="1"
Margin="-24,0,-24,0"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<!-- Dock Section Selector -->
<StackPanel Spacing="8">
<TextBlock
x:Uid="PinToDock_SectionHeader"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<tkcontrols:Segmented
x:Name="SectionSegmented"
HorizontalAlignment="Stretch"
SelectedIndex="0"
SelectionMode="Single">
<tkcontrols:SegmentedItem x:Name="StartSegmentedItem" x:Uid="PinToDock_Start" />
<tkcontrols:SegmentedItem x:Name="CenterSegmentedItem" x:Uid="PinToDock_Center" />
<tkcontrols:SegmentedItem x:Name="EndSegmentedItem" x:Uid="PinToDock_End" />
</tkcontrols:Segmented>
</StackPanel>
<!-- Label Options -->
<StackPanel Spacing="4">
<CheckBox
x:Name="ShowTitleCheckBox"
x:Uid="PinToDock_ShowTitle"
Checked="OnLabelOptionChanged"
IsChecked="True"
Unchecked="OnLabelOptionChanged" />
<CheckBox
x:Name="ShowSubtitleCheckBox"
x:Uid="PinToDock_ShowSubtitle"
Checked="OnLabelOptionChanged"
IsChecked="True"
Unchecked="OnLabelOptionChanged" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,171 @@
// 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.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI.Dock;
public sealed partial class PinToDockDialogContent : UserControl
{
private string _title = string.Empty;
private string _subtitle = string.Empty;
public DockPinSide SelectedSide => SectionSegmented.SelectedIndex switch
{
0 => DockPinSide.Start,
1 => DockPinSide.Center,
2 => DockPinSide.End,
_ => DockPinSide.Start,
};
public bool? ShowTitles => ShowTitleCheckBox.IsChecked;
public bool? ShowSubtitles => ShowSubtitleCheckBox.IsChecked;
public PinToDockDialogContent()
{
InitializeComponent();
}
public void Configure(string title, string subtitle, IconInfoViewModel? icon, DockSide dockSide)
{
_title = title;
_subtitle = subtitle;
var hasTitle = !string.IsNullOrEmpty(title);
var hasSubtitle = !string.IsNullOrEmpty(subtitle);
PreviewTitleText.Text = title;
PreviewTitleText.Visibility = hasTitle ? Visibility.Visible : Visibility.Collapsed;
PreviewSubtitleText.Text = subtitle;
PreviewSubtitleText.Visibility = hasSubtitle ? Visibility.Visible : Visibility.Collapsed;
PreviewTextPanel.Visibility = (hasTitle || hasSubtitle) ? Visibility.Visible : Visibility.Collapsed;
ShowTitleCheckBox.Visibility = hasTitle ? Visibility.Visible : Visibility.Collapsed;
ShowTitleCheckBox.IsChecked = hasTitle;
ShowSubtitleCheckBox.Visibility = hasSubtitle ? Visibility.Visible : Visibility.Collapsed;
ShowSubtitleCheckBox.IsChecked = hasSubtitle;
if (icon is not null)
{
PreviewIcon.SourceKey = icon;
}
ApplyDockOrientation(dockSide);
}
public static async System.Threading.Tasks.Task<(ContentDialogResult Result, PinToDockDialogContent Content)> ShowAsync(
XamlRoot xamlRoot,
string title,
string subtitle,
IconInfoViewModel? icon,
DockSide dockSide)
{
var content = new PinToDockDialogContent();
content.Configure(title, subtitle, icon, dockSide);
var dialog = new ContentDialog
{
Title = RS_.GetString("PinToDock_DialogTitle"),
Content = content,
PrimaryButtonText = RS_.GetString("PinToDock_PinButton"),
CloseButtonText = RS_.GetString("PinToDock_CancelButton"),
DefaultButton = ContentDialogButton.Primary,
XamlRoot = xamlRoot,
};
// Inner controls (Segmented, CheckBox) may consume the Enter key event,
// preventing DefaultButton from activating. Handle it explicitly.
var enterPressed = false;
dialog.AddHandler(
UIElement.KeyDownEvent,
new KeyEventHandler((s, e) =>
{
if (e.Key == VirtualKey.Enter)
{
enterPressed = true;
dialog.Hide();
}
}),
true);
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.None && enterPressed)
{
result = ContentDialogResult.Primary;
}
return (result, content);
}
private void ApplyDockOrientation(DockSide dockSide)
{
var isVertical = dockSide is DockSide.Left or DockSide.Right;
if (isVertical)
{
StartSegmentedItem.Content = RS_.GetString("PinToDock_Top");
CenterSegmentedItem.Content = RS_.GetString("PinToDock_CenterLabel");
EndSegmentedItem.Content = RS_.GetString("PinToDock_Bottom");
}
else
{
StartSegmentedItem.Content = RS_.GetString("PinToDock_Left");
CenterSegmentedItem.Content = RS_.GetString("PinToDock_CenterLabel");
EndSegmentedItem.Content = RS_.GetString("PinToDock_RightLabel");
}
// Pick the 3 icon path strings based on dock orientation
var (startKey, centerKey, endKey) = dockSide switch
{
DockSide.Top => ("TopStartPath", "TopCenterPath", "TopEndPath"),
DockSide.Bottom => ("BottomStartPath", "BottomCenterPath", "BottomEndPath"),
DockSide.Left => ("LeftStartPath", "LeftCenterPath", "LeftEndPath"),
DockSide.Right => ("RightStartPath", "RightCenterPath", "RightEndPath"),
_ => ("TopStartPath", "TopCenterPath", "TopEndPath"),
};
StartSegmentedItem.Icon = CreatePathIcon((string)Resources[startKey]);
CenterSegmentedItem.Icon = CreatePathIcon((string)Resources[centerKey]);
EndSegmentedItem.Icon = CreatePathIcon((string)Resources[endKey]);
}
private static PathIcon CreatePathIcon(string pathData)
{
var geometry = (Microsoft.UI.Xaml.Media.Geometry)Microsoft.UI.Xaml.Markup.XamlBindingHelper.ConvertValue(
typeof(Microsoft.UI.Xaml.Media.Geometry), pathData);
return new PathIcon { Data = geometry };
}
private void OnLabelOptionChanged(object sender, RoutedEventArgs e)
{
if (PreviewTitleText is null || PreviewSubtitleText is null ||
PreviewTextPanel is null || ShowTitleCheckBox is null || ShowSubtitleCheckBox is null)
{
return;
}
var showTitle = ShowTitleCheckBox.IsChecked == true;
var showSubtitle = ShowSubtitleCheckBox.IsChecked == true;
PreviewTitleText.Text = showTitle ? _title : string.Empty;
PreviewTitleText.Visibility = showTitle ? Visibility.Visible : Visibility.Collapsed;
PreviewSubtitleText.Text = showSubtitle ? _subtitle : string.Empty;
PreviewSubtitleText.Visibility = showSubtitle ? Visibility.Visible : Visibility.Collapsed;
PreviewTextPanel.Visibility = (showTitle || showSubtitle) ? Visibility.Visible : Visibility.Collapsed;
}
}

View File

@@ -156,21 +156,7 @@ public sealed partial class ListPage : Page,
/// </summary>
private int GetFirstSelectableIndex()
{
var items = ItemView.Items;
if (items is null || items.Count == 0)
{
return -1;
}
for (var i = 0; i < items.Count; i++)
{
if (!IsSeparator(items[i]))
{
return i;
}
}
return -1;
return FindSelectableIndex(0, 1);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
@@ -604,9 +590,44 @@ public sealed partial class ListPage : Page,
? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
: Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));
targetIndex = FindSelectableIndexForPageNavigation(targetIndex, isPageDown);
if (targetIndex == -1)
{
return null;
}
return (currentIndex, targetIndex);
}
private int FindSelectableIndex(int startIndex, int step)
{
var items = ItemView.Items;
var count = items.Count;
if (count == 0 || step == 0)
{
return -1;
}
startIndex = Math.Clamp(startIndex, 0, count - 1);
for (var i = startIndex; i >= 0 && i < count; i += step)
{
if (!IsSeparator(items[i]))
{
return i;
}
}
return -1;
}
private int FindSelectableIndexForPageNavigation(int targetIndex, bool isPageDown)
{
var step = isPageDown ? 1 : -1;
var index = FindSelectableIndex(targetIndex, step);
return index != -1 ? index : FindSelectableIndex(targetIndex - step, -step);
}
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ListPage @this)
@@ -1045,39 +1066,24 @@ public sealed partial class ListPage : Page,
/// </summary>
private void NavigateUp()
{
var newIndex = ItemView.SelectedIndex;
if (ItemView.SelectedIndex > 0)
if (ItemView.Items.Count == 0)
{
newIndex--;
while (
newIndex >= 0 &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex--;
}
if (newIndex < 0)
{
newIndex = ItemView.Items.Count - 1;
while (
newIndex >= 0 &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex--;
}
}
}
else
{
newIndex = ItemView.Items.Count - 1;
return;
}
ItemView.SelectedIndex = newIndex;
var newIndex = ItemView.SelectedIndex > 0
? FindSelectableIndex(ItemView.SelectedIndex - 1, -1)
: -1;
if (newIndex == -1)
{
newIndex = FindSelectableIndex(ItemView.Items.Count - 1, -1);
}
if (newIndex != -1)
{
ItemView.SelectedIndex = newIndex;
}
}
private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
@@ -1168,50 +1174,24 @@ public sealed partial class ListPage : Page,
/// </summary>
private void NavigateDown()
{
var newIndex = ItemView.SelectedIndex;
if (ItemView.SelectedIndex == ItemView.Items.Count - 1)
if (ItemView.Items.Count == 0)
{
newIndex = 0;
while (
newIndex < ItemView.Items.Count &&
IsSeparator(ItemView.Items[newIndex]))
{
newIndex++;
}
if (newIndex >= ItemView.Items.Count)
{
return;
}
}
else
{
newIndex++;
while (
newIndex < ItemView.Items.Count &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex++;
}
if (newIndex >= ItemView.Items.Count)
{
newIndex = 0;
while (
newIndex < ItemView.Items.Count &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex++;
}
}
return;
}
ItemView.SelectedIndex = newIndex;
var newIndex = ItemView.SelectedIndex < ItemView.Items.Count - 1
? FindSelectableIndex(ItemView.SelectedIndex + 1, 1)
: -1;
if (newIndex == -1)
{
newIndex = FindSelectableIndex(0, 1);
}
if (newIndex != -1)
{
ItemView.SelectedIndex = newIndex;
}
}
private bool IsSeparator(object? item) => item is ListItemViewModel li && !li.IsInteractive;

View File

@@ -10,8 +10,8 @@
xmlns:winuiex="using:WinUIEx"
Width="800"
Height="480"
MinWidth="320"
MinHeight="240"
MinWidth="640"
MinHeight="420"
Activated="MainWindow_Activated"
Closed="MainWindow_Closed"
mc:Ignorable="d">

View File

@@ -105,6 +105,7 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />

View File

@@ -26,7 +26,6 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.UI.Core;
using WinUIEx;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using VirtualKey = Windows.System.VirtualKey;
@@ -51,6 +50,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
IRecipient<ShowToastMessage>,
IRecipient<NavigateToPageMessage>,
IRecipient<ShowHideDockMessage>,
IRecipient<ShowPinToDockDialogMessage>,
INotifyPropertyChanged,
IDisposable
{
@@ -72,6 +72,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private CancellationTokenSource? _focusAfterLoadedCts;
private WeakReference<Page>? _lastNavigatedPageRef;
private bool _isDisposed;
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
@@ -102,6 +103,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
WeakReferenceMessenger.Default.Register<ShowPinToDockDialogMessage>(this);
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
@@ -210,6 +212,43 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
});
}
public void Receive(ShowPinToDockDialogMessage message)
{
DispatcherQueue.TryEnqueue(async () =>
{
try
{
await HandlePinToDockDialogOnUiThread(message);
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
});
}
private async Task HandlePinToDockDialogOnUiThread(ShowPinToDockDialogMessage message)
{
var (result, content) = await PinToDockDialogContent.ShowAsync(
this.XamlRoot,
message.Title,
message.Subtitle,
message.Icon,
message.DockSide);
if (result == ContentDialogResult.Primary)
{
var pinMessage = new PinToDockMessage(
message.ProviderId,
message.CommandId,
Pin: true,
Side: content.SelectedSide,
ShowTitles: content.ShowTitles,
ShowSubtitles: content.ShowSubtitles);
WeakReferenceMessenger.Default.Send(pinMessage);
}
}
// This gets called from the UI thread
private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args)
{
@@ -269,6 +308,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
_ = DispatcherQueue.TryEnqueue(() =>
{
if (_isDisposed)
{
return;
}
OpenSettings(message.SettingsPageTag);
});
}
@@ -440,20 +484,23 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// However, then we have more fine-grained control on the back stack, managing the VM cache, and not
// having that all be a black box, though then we wouldn't cache the XAML page itself, but sometimes that is a drawback.
// However, we do a good job here, see ForwardStack.Clear below, and BackStack.Clear above about managing that.
if (withAnimation)
if (RootFrame.CanGoBack)
{
RootFrame.GoBack();
}
else
{
RootFrame.GoBack(_noAnimation);
}
if (withAnimation)
{
RootFrame.GoBack();
}
else
{
RootFrame.GoBack(_noAnimation);
}
// Don't store pages we're navigating away from in the Frame cache
// TODO: In the future we probably want a short cache (3-5?) of recent VMs in case the user re-navigates
// back to a recent page they visited (like the Pokedex) so we don't have to reload it from scratch.
// That'd be retrieved as we re-navigate in the PerformCommandMessage logic above
RootFrame.ForwardStack.Clear();
// Don't store pages we're navigating away from in the Frame cache
// TODO: In the future we probably want a short cache (3-5?) of recent VMs in case the user re-navigates
// back to a recent page they visited (like the Pokedex) so we don't have to reload it from scratch.
// That'd be retrieved as we re-navigate in the PerformCommandMessage logic above
RootFrame.ForwardStack.Clear();
}
if (!RootFrame.CanGoBack)
{
@@ -489,6 +536,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
_ = DispatcherQueue.TryEnqueue(() =>
{
if (_isDisposed)
{
return;
}
if (message.ShowDock)
{
if (_dockWindow is null)
@@ -790,10 +842,33 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
WeakReferenceMessenger.Default.UnregisterAll(this);
_focusAfterLoadedCts?.Cancel();
_focusAfterLoadedCts?.Dispose();
_focusAfterLoadedCts = null;
_dockWindow?.Dispose();
var dockWindow = _dockWindow;
_dockWindow = null;
if (dockWindow is not null)
{
if (DispatcherQueue.HasThreadAccess)
{
dockWindow.Close();
}
else
{
DispatcherQueue.TryEnqueue(dockWindow.Close);
}
}
GC.SuppressFinalize(this);
}
}

View File

@@ -216,6 +216,7 @@
<ItemsRepeater
x:Name="ProvidersRepeater"
x:Load="{x:Bind viewModel.Extensions.HasResults, Mode=OneWay}"
GettingFocus="ProvidersRepeater_GettingFocus"
ItemsSource="{x:Bind viewModel.Extensions.FilteredProviders, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}">
<ItemsRepeater.ItemTemplate>
@@ -224,6 +225,7 @@
Click="SettingsCard_Click"
DataContext="{x:Bind}"
Description="{x:Bind ExtensionSubtext, Mode=OneWay}"
GotFocus="SettingsCard_GotFocus"
Header="{x:Bind DisplayName, Mode=OneWay}"
IsClickEnabled="True">
<controls:SettingsCard.HeaderIcon>

View File

@@ -19,6 +19,7 @@ public sealed partial class ExtensionsPage : Page
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private readonly SettingsViewModel? viewModel;
private int _lastFocusedIndex;
public ExtensionsPage()
{
@@ -58,4 +59,157 @@ public sealed partial class ExtensionsPage : Page
Logger.LogError("Error when showing FallbackRankerDialog", ex);
}
}
private void SettingsCard_GotFocus(object sender, RoutedEventArgs e)
{
// Track focus whenever any part of the card gets focus (including children like ToggleSwitch)
if (sender is SettingsCard card && viewModel is not null)
{
var dataContext = card.DataContext as ProviderSettingsViewModel;
if (dataContext is not null)
{
var filteredProviders = viewModel.Extensions.FilteredProviders;
var index = filteredProviders.IndexOf(dataContext);
if (index >= 0)
{
_lastFocusedIndex = index;
}
}
}
}
private void ProvidersRepeater_GettingFocus(UIElement sender, GettingFocusEventArgs args)
{
if (viewModel is null || ProvidersRepeater is null)
{
return;
}
// Only intervene when focus is coming into the ItemsRepeater from outside
if (args.OldFocusedElement != null && IsElementInsideRepeater(args.OldFocusedElement))
{
return;
}
var filteredProviders = viewModel.Extensions.FilteredProviders;
if (filteredProviders.Count == 0)
{
return;
}
// Get the last focused index, defaulting to 0
var index = _lastFocusedIndex;
if (index < 0 || index >= filteredProviders.Count)
{
index = 0;
}
// Check if WinUI is trying to focus something other than our target
var shouldIntervene = false;
// If direction is Previous (Shift+Tab), we need to intervene
if (args.Direction == FocusNavigationDirection.Previous || args.Direction == FocusNavigationDirection.Up)
{
shouldIntervene = true;
}
// Also intervene if the NewFocusedElement is not at our target index
else if (args.NewFocusedElement is DependencyObject newFocus)
{
// Check if the new focus element is inside our target card
var targetCard = ProvidersRepeater.TryGetElement(index) as SettingsCard;
if (targetCard != null && !IsElementInsideCard(newFocus, targetCard))
{
shouldIntervene = true;
}
}
if (shouldIntervene)
{
// Ensure the target element is realized before trying to focus it
ProvidersRepeater.GetOrCreateElement(index);
// Get the target card
var targetCard = ProvidersRepeater.TryGetElement(index) as SettingsCard;
if (targetCard != null)
{
// For shift-tab or wrong target, cancel and manually set focus
args.TryCancel();
args.Handled = true;
// Set focus asynchronously to the target card and scroll it into view
_ = targetCard.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
targetCard.Focus(FocusState.Keyboard);
BringCardIntoView(targetCard);
});
}
}
else
{
// For normal Tab forward, just redirect
ProvidersRepeater.GetOrCreateElement(index);
var targetCard = ProvidersRepeater.TryGetElement(index) as SettingsCard;
if (targetCard != null)
{
args.TrySetNewFocusedElement(targetCard);
args.Handled = true;
// Set focus asynchronously to the target card and scroll it into view
_ = targetCard.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
BringCardIntoView(targetCard);
});
}
}
}
private void BringCardIntoView(SettingsCard card)
{
card.StartBringIntoView(new BringIntoViewOptions
{
AnimationDesired = true,
VerticalAlignmentRatio = 0.5, // Center vertically
});
}
private bool IsElementInsideCard(DependencyObject element, SettingsCard card)
{
var parent = element;
while (parent != null)
{
if (ReferenceEquals(parent, card))
{
return true;
}
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
}
return false;
}
private bool IsElementInsideRepeater(object element)
{
if (element is not DependencyObject depObj)
{
return false;
}
var parent = depObj;
while (parent != null)
{
if (ReferenceEquals(parent, ProvidersRepeater))
{
return true;
}
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
}
return false;
}
}

View File

@@ -989,4 +989,68 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>NEW</value>
<comment>Must be all caps</comment>
</data>
<data name="PinToDock_DialogTitle" xml:space="preserve">
<value>Pin to Dock</value>
<comment>Title for the pin to dock configuration dialog</comment>
</data>
<data name="PinToDock_PreviewHeader.Text" xml:space="preserve">
<value>Preview</value>
<comment>Header for the preview section in the pin to dock dialog</comment>
</data>
<data name="PinToDock_SectionHeader.Text" xml:space="preserve">
<value>Choose where to pin this command</value>
<comment>Header for the dock section selector in the pin to dock dialog</comment>
</data>
<data name="PinToDock_Start.Content" xml:space="preserve">
<value>Start</value>
<comment>Start section option in pin to dock dialog</comment>
</data>
<data name="PinToDock_Center.Content" xml:space="preserve">
<value>Center</value>
<comment>Center section option in pin to dock dialog</comment>
</data>
<data name="PinToDock_End.Content" xml:space="preserve">
<value>End</value>
<comment>End section option in pin to dock dialog</comment>
</data>
<data name="PinToDock_ShowTitle.Content" xml:space="preserve">
<value>Show title</value>
<comment>Checkbox label to show the title of a pinned dock item</comment>
</data>
<data name="PinToDock_ShowSubtitle.Content" xml:space="preserve">
<value>Show subtitle</value>
<comment>Checkbox label to show the subtitle of a pinned dock item</comment>
</data>
<data name="PinToDock_PinButton" xml:space="preserve">
<value>Pin</value>
<comment>Button text to confirm pinning an item to the dock</comment>
</data>
<data name="PinToDock_CancelButton" xml:space="preserve">
<value>Cancel</value>
<comment>Button text to cancel pinning an item to the dock</comment>
</data>
<data name="PinToDock_Left" xml:space="preserve">
<value>Left</value>
<comment>Left section option in pin to dock dialog (horizontal dock)</comment>
</data>
<data name="PinToDock_Right" xml:space="preserve">
<value>Right</value>
<comment>Right section option in pin to dock dialog (horizontal dock)</comment>
</data>
<data name="PinToDock_Top" xml:space="preserve">
<value>Top</value>
<comment>Top section option in pin to dock dialog (vertical dock)</comment>
</data>
<data name="PinToDock_Bottom" xml:space="preserve">
<value>Bottom</value>
<comment>Bottom section option in pin to dock dialog (vertical dock)</comment>
</data>
<data name="PinToDock_CenterLabel" xml:space="preserve">
<value>Center</value>
<comment>Center section label in pin to dock dialog (code access)</comment>
</data>
<data name="PinToDock_RightLabel" xml:space="preserve">
<value>Right</value>
<comment>Right section label in pin to dock dialog (code access, horizontal end)</comment>
</data>
</root>

View File

@@ -68,7 +68,7 @@ public class QueryTests : CommandPaletteUnitTestBase
{
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, input);
page.SearchText = input;
var resultLists = page.GetItems();
var result = Query(input, resultLists);
@@ -117,7 +117,7 @@ public class QueryTests : CommandPaletteUnitTestBase
{
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, input);
page.SearchText = input;
var resultLists = page.GetItems();
var firstItem = resultLists.FirstOrDefault();
@@ -158,7 +158,7 @@ public class QueryTests : CommandPaletteUnitTestBase
{
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, query);
page.SearchText = query;
var results = page.GetItems();
// Assert
@@ -176,7 +176,8 @@ public class QueryTests : CommandPaletteUnitTestBase
{
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText("abc", input);
page.SearchText = "abc";
page.SearchText = input;
var results = page.GetItems();
// Assert
@@ -193,7 +194,7 @@ public class QueryTests : CommandPaletteUnitTestBase
{
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, query);
page.SearchText = query;
var resultsList = page.GetItems();
var results = Query(query, resultsList);
@@ -211,7 +212,7 @@ public class QueryTests : CommandPaletteUnitTestBase
{
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, query);
page.SearchText = query;
var resultsList = page.GetItems();
// Assert
@@ -219,4 +220,28 @@ public class QueryTests : CommandPaletteUnitTestBase
var firstResult = resultsList.FirstOrDefault();
Assert.IsTrue(firstResult.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
}
[TestMethod]
public void UpdateSearchTextMatchesSearchTextSetter()
{
var settings = new Settings();
var pageUsingSetter = new TimeDateExtensionPage(settings);
var pageUsingMethod = new TimeDateExtensionPage(settings);
const string query = "time::12:30:45";
pageUsingSetter.SearchText = query;
pageUsingMethod.UpdateSearchText(string.Empty, query);
var setterResults = pageUsingSetter.GetItems();
var methodResults = pageUsingMethod.GetItems();
Assert.AreEqual(setterResults.Length, methodResults.Length, "UpdateSearchText should produce the same number of results as setting SearchText.");
var setterFirstItem = setterResults.FirstOrDefault();
var methodFirstItem = methodResults.FirstOrDefault();
Assert.IsNotNull(setterFirstItem);
Assert.IsNotNull(methodFirstItem);
Assert.AreEqual(setterFirstItem.Title, methodFirstItem.Title, "UpdateSearchText should keep the stored query in sync with SearchText.");
Assert.AreEqual(setterFirstItem.Subtitle, methodFirstItem.Subtitle, "UpdateSearchText should keep the stored query in sync with SearchText.");
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.CmdPal.Common.Text;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -76,4 +77,64 @@ public class CommandItemViewModelTests
Assert.IsNotNull(viewModel.SecondaryCommand);
Assert.AreEqual("Secondary", viewModel.SecondaryCommand.Name);
}
[TestMethod]
public void LatePrimaryCommandCreation_AddsPrimaryToAllCommands()
{
// Reproduces issue where SlowInitializeProperties runs before a real primary command exists.
// The late-arriving command should still create the synthetic primary context item and prepend it to AllCommands.
var pageContext = new TestPageContext();
var item = new CommandItem()
{
Command = null,
MoreCommands =
[
new CommandContextItem(new NoOpCommand { Name = "Secondary" }),
],
};
var viewModel = new CommandItemViewModel(new(item), new(pageContext), DefaultContextMenuFactory.Instance);
viewModel.SlowInitializeProperties();
Assert.AreEqual(1, viewModel.AllCommands.Count);
Assert.AreEqual("Secondary", ((CommandContextItemViewModel)viewModel.AllCommands[0]).Name);
item.Command = new NoOpCommand { Name = "Primary" };
Assert.AreEqual(2, viewModel.AllCommands.Count);
Assert.AreEqual("Primary", ((CommandContextItemViewModel)viewModel.AllCommands[0]).Name);
Assert.AreEqual("Secondary", ((CommandContextItemViewModel)viewModel.AllCommands[1]).Name);
Assert.IsTrue(viewModel.HasMoreCommands);
Assert.AreEqual("Secondary", viewModel.SecondaryCommand?.Name);
}
[TestMethod]
public void SyntheticPrimaryContextItem_UpdatesSubtitleAndCachedSubtitleTarget()
{
// The synthetic primary context item copies subtitle state from the parent CommandItemViewModel.
// When subtitle changes later, both the exposed subtitle and its cached fuzzy-search target must refresh.
var pageContext = new TestPageContext();
var item = new CommandItem(new NoOpCommand { Name = "Primary" })
{
Subtitle = "before",
MoreCommands =
[
new CommandContextItem(new NoOpCommand { Name = "Secondary" }),
],
};
var viewModel = new CommandItemViewModel(new(item), new(pageContext), DefaultContextMenuFactory.Instance);
viewModel.SlowInitializeProperties();
var primaryContextItem = (CommandContextItemViewModel)viewModel.AllCommands[0];
var matcher = new PrecomputedFuzzyMatcher(new PrecomputedFuzzyMatcherOptions());
Assert.AreEqual("before", primaryContextItem.Subtitle);
Assert.AreEqual("before", primaryContextItem.GetSubtitleTarget(matcher).Original);
item.Subtitle = "after unique";
Assert.AreEqual("after unique", primaryContextItem.Subtitle);
Assert.AreEqual("after unique", primaryContextItem.GetSubtitleTarget(matcher).Original);
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Commands;
internal sealed partial class ToggleKeyboardManagerListeningCommand : InvokableCommand
{
public ToggleKeyboardManagerListeningCommand()
{
Name = "Toggle Keyboard Manager active state";
}
public override CommandResult Invoke()
{
return KeyboardManagerStateService.TryToggleListening()
? CommandResult.KeepOpen()
: CommandResult.ShowToast(Resources.ResourceManager.GetString("KeyboardManager_ToggleListening_Error", Resources.Culture) ?? "Keyboard Manager is unavailable. Try enabling it in PowerToys settings.");
}
}

View File

@@ -43,21 +43,6 @@ internal static class KeyboardManagerStateService
return false;
}
internal static bool TryToggleListening()
{
try
{
using var evt = EventWaitHandle.OpenExisting(Constants.ToggleKeyboardManagerActiveEvent());
var signaled = evt.Set();
PollStatus();
return signaled;
}
catch
{
return false;
}
}
private static void PollStatus()
{
var isListening = IsListening();

View File

@@ -14,11 +14,6 @@ internal static class PowerToysResourcesHelper
internal static IconInfo IconFromSettingsIcon(string fileName) => IconHelpers.FromRelativePath($"{SettingsIconRoot}{fileName}");
internal static IconInfo KeyboardManagerListeningIcon(bool isListening) => IconHelpers.FromRelativePath(
isListening
? $"{AssetsRoot}KeyboardManager\\KeyboardManagerListeningOn.svg"
: $"{AssetsRoot}KeyboardManager\\KeyboardManagerListeningOff.svg");
#if DEBUG
public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.dark.png");
#else

View File

@@ -22,22 +22,9 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
var title = module.ModuleDisplayName();
var icon = module.ModuleIcon();
if (ModuleEnablementService.IsModuleEnabled(module))
{
var isListening = KeyboardManagerStateService.IsListening();
yield return new ListItem(new ToggleKeyboardManagerListeningCommand() { Id = "com.microsoft.powertoys.keyboardManager.toggleListening" })
{
Title = GetResourceString("KeyboardManager_ToggleListening_Title", "Keyboard Manager: Toggle active state"),
Subtitle = isListening
? GetResourceString("KeyboardManager_ToggleListening_On_Subtitle", "Keyboard Manager is active. Invoke to stop listening.")
: GetResourceString("KeyboardManager_ToggleListening_Off_Subtitle", "Keyboard Manager is paused. Invoke to start listening."),
Icon = PowerToysResourcesHelper.KeyboardManagerListeningIcon(isListening),
};
}
if (IsUseNewEditorEnabled())
{
yield return new ListItem(new OpenNewKeyboardManagerEditorCommand())
yield return new ListItem(new OpenNewKeyboardManagerEditorCommand() { Id = "com.microsoft.powertoys.keyboardManager.openNewEditor" })
{
Title = Resources.KeyboardManager_OpenNewEditor_Title,
Subtitle = Resources.KeyboardManager_OpenNewEditor_Subtitle,

View File

@@ -414,18 +414,6 @@
<data name="KeyboardManager_OpenNewEditor_Subtitle" xml:space="preserve">
<value>Open the Keyboard Manager remap editor</value>
</data>
<data name="KeyboardManager_ToggleListening_Title" xml:space="preserve">
<value>Keyboard Manager: Toggle active state</value>
</data>
<data name="KeyboardManager_ToggleListening_On_Subtitle" xml:space="preserve">
<value>Keyboard Manager is active. Invoke to stop listening.</value>
</data>
<data name="KeyboardManager_ToggleListening_Off_Subtitle" xml:space="preserve">
<value>Keyboard Manager is paused. Invoke to start listening.</value>
</data>
<data name="KeyboardManager_ToggleListening_Error" xml:space="preserve">
<value>Keyboard Manager is unavailable. Try enabling it in PowerToys settings.</value>
</data>
<!-- Light Switch Module -->
<data name="LightSwitch_Toggle_Title" xml:space="preserve">
<value>Light Switch: Toggle theme</value>

View File

@@ -3,9 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -14,12 +11,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate.Pages;
internal sealed partial class TimeDateExtensionPage : DynamicListPage
{
private readonly Lock _resultsLock = new();
private IList<ListItem> _results = new List<ListItem>();
private bool _dataLoaded;
private ISettingsInterface _settingsManager;
private readonly ISettingsInterface _settingsManager;
public TimeDateExtensionPage(ISettingsInterface settingsManager)
{
@@ -33,39 +25,10 @@ internal sealed partial class TimeDateExtensionPage : DynamicListPage
}
public override IListItem[] GetItems()
{
ListItem[] results;
lock (_resultsLock)
{
if (_dataLoaded)
{
results = _results.ToArray();
_dataLoaded = false;
return results;
}
}
DoExecuteSearch(string.Empty);
lock (_resultsLock)
{
results = _results.ToArray();
_dataLoaded = false;
return results;
}
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
DoExecuteSearch(newSearch);
}
private void DoExecuteSearch(string query)
{
try
{
var result = TimeDateCalculator.ExecuteSearch(_settingsManager, query);
UpdateResult(result);
return [.. TimeDateCalculator.ExecuteSearch(_settingsManager, SearchText)];
}
catch (Exception)
{
@@ -74,23 +37,13 @@ internal sealed partial class TimeDateExtensionPage : DynamicListPage
// So, we need to clean the result.
// But in that time, empty result may cause exception.
// So, we need to add at least on item to user.
var items = new List<ListItem>
{
ResultHelper.CreateInvalidInputErrorResult(),
};
UpdateResult(items);
return [ResultHelper.CreateInvalidInputErrorResult()];
}
}
private void UpdateResult(IList<ListItem> result)
public override void UpdateSearchText(string oldSearch, string newSearch)
{
lock (_resultsLock)
{
this._results = result;
_dataLoaded = true;
}
RaiseItemsChanged(this._results.Count);
SetSearchNoUpdate(newSearch);
RaiseItemsChanged(-2);
}
}

View File

@@ -564,7 +564,7 @@ namespace KeyboardManagerEditorUI.Controls
return CurrentActionType switch
{
ActionType.KeyOrShortcut => _actionKeys.Count > 0,
ActionType.Text => !string.IsNullOrWhiteSpace(TextContentBox?.Text),
ActionType.Text => !string.IsNullOrEmpty(TextContentBox?.Text),
ActionType.OpenUrl => !string.IsNullOrWhiteSpace(UrlPathInput?.Text),
ActionType.OpenApp => !string.IsNullOrWhiteSpace(ProgramPathInput?.Text),
_ => false,

View File

@@ -90,7 +90,7 @@ namespace KeyboardManagerEditorUI.Helpers
return ValidationErrorType.EmptyOriginalKeys;
}
if (string.IsNullOrWhiteSpace(textContent))
if (string.IsNullOrEmpty(textContent))
{
return ValidationErrorType.EmptyTargetText;
}

View File

@@ -38,7 +38,6 @@ namespace
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut";
const wchar_t JSON_KEY_EDITOR_SHORTCUT[] = L"EditorShortcut";
const wchar_t JSON_KEY_USE_NEW_EDITOR[] = L"useNewEditor";
}
@@ -57,25 +56,21 @@ private:
//contains the non localized key of the powertoy
std::wstring app_key = KeyboardManagerConstants::ModuleName;
// Hotkey for toggling the module
Hotkey m_hotkey = { .key = 0 };
// Hotkey for opening the editor
Hotkey m_editorHotkey = { .key = 0 };
// Whether to use the new WinUI3 editor
bool m_useNewEditor = false;
ULONGLONG m_lastHotkeyToggleTime = 0;
ULONGLONG m_lastHotkeyTime = 0;
HANDLE m_hProcess = nullptr;
HANDLE m_hEditorProcess = nullptr;
HANDLE m_hTerminateEngineEvent = nullptr;
HANDLE m_open_new_editor_event_handle{ nullptr };
HANDLE m_toggle_active_event_handle{ nullptr };
std::thread m_toggle_thread;
std::atomic<bool> m_toggle_thread_running{ false };
std::thread m_editor_listener_thread;
std::atomic<bool> m_editor_listener_running{ false };
void refresh_process_state()
@@ -88,19 +83,6 @@ private:
}
}
void toggle_engine()
{
refresh_process_state();
if (m_active)
{
stop_engine();
}
else
{
start_engine();
}
}
bool start_engine()
{
refresh_process_state();
@@ -176,35 +158,9 @@ private:
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES)
.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
Logger::error("Failed to initialize Keyboard Manager toggle shortcut");
}
}
if (!m_hotkey.key)
{
// Set default: Win+Shift+K
m_hotkey.win = true;
m_hotkey.shift = true;
m_hotkey.ctrl = false;
m_hotkey.alt = false;
m_hotkey.key = 'K';
}
// Parse editor shortcut
bool editorShortcutParsed = false;
if (settingsObject.GetView().Size())
{
try
@@ -216,6 +172,7 @@ private:
m_editorHotkey.shift = jsonEditorHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_editorHotkey.ctrl = jsonEditorHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_editorHotkey.key = static_cast<unsigned char>(jsonEditorHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
editorShortcutParsed = true;
}
catch (...)
{
@@ -223,9 +180,9 @@ private:
}
}
if (!m_editorHotkey.key)
if (!editorShortcutParsed && !m_editorHotkey.key)
{
// Set default: Win+Shift+Q
// Set default: Win+Shift+Q (only when no setting exists)
m_editorHotkey.win = true;
m_editorHotkey.shift = true;
m_editorHotkey.ctrl = false;
@@ -287,7 +244,6 @@ public:
}
m_open_new_editor_event_handle = CreateDefaultEvent(CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT);
m_toggle_active_event_handle = CreateDefaultEvent(CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT);
init_settings();
};
@@ -306,11 +262,6 @@ public:
CloseHandle(m_open_new_editor_event_handle);
m_open_new_editor_event_handle = nullptr;
}
if (m_toggle_active_event_handle)
{
CloseHandle(m_toggle_active_event_handle);
m_toggle_active_event_handle = nullptr;
}
if (m_hEditorProcess)
{
CloseHandle(m_hEditorProcess);
@@ -412,22 +363,12 @@ public:
return false;
}
// Return the invocation hotkeys for toggling and opening the editor
// Return the invocation hotkey for opening the editor
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
size_t count = 0;
// Hotkey 0: toggle engine
if (m_hotkey.key)
{
if (hotkeys && buffer_size > count)
{
hotkeys[count] = m_hotkey;
}
count++;
}
// Hotkey 1: open editor (only when using new editor)
// Hotkey 0: open editor (only when using new editor)
if (m_useNewEditor && m_editorHotkey.key)
{
if (hotkeys && buffer_size > count)
@@ -442,69 +383,44 @@ public:
void StartOpenEditorListener()
{
if (m_toggle_thread_running || (!m_open_new_editor_event_handle && !m_toggle_active_event_handle))
if (m_editor_listener_running || !m_open_new_editor_event_handle)
{
return;
}
m_toggle_thread_running = true;
m_toggle_thread = std::thread([this]() {
HANDLE handles[2]{};
DWORD handle_count = 0;
DWORD open_editor_index = MAXDWORD;
DWORD toggle_active_index = MAXDWORD;
if (m_open_new_editor_event_handle)
m_editor_listener_running = true;
m_editor_listener_thread = std::thread([this]() {
while (m_editor_listener_running)
{
open_editor_index = handle_count;
handles[handle_count++] = m_open_new_editor_event_handle;
}
if (m_toggle_active_event_handle)
{
toggle_active_index = handle_count;
handles[handle_count++] = m_toggle_active_event_handle;
}
while (m_toggle_thread_running)
{
const DWORD wait_result = WaitForMultipleObjects(handle_count, handles, FALSE, 500);
if (!m_toggle_thread_running)
const DWORD wait_result = WaitForSingleObject(m_open_new_editor_event_handle, 500);
if (!m_editor_listener_running)
{
break;
}
if (open_editor_index != MAXDWORD && wait_result == (WAIT_OBJECT_0 + open_editor_index))
if (wait_result == WAIT_OBJECT_0)
{
launch_editor();
}
else if (toggle_active_index != MAXDWORD && wait_result == (WAIT_OBJECT_0 + toggle_active_index))
{
toggle_engine();
}
}
});
}
void StopOpenEditorListener()
{
if (!m_toggle_thread_running)
if (!m_editor_listener_running)
{
return;
}
m_toggle_thread_running = false;
m_editor_listener_running = false;
if (m_open_new_editor_event_handle)
{
SetEvent(m_open_new_editor_event_handle);
}
if (m_toggle_active_event_handle)
if (m_editor_listener_thread.joinable())
{
SetEvent(m_toggle_active_event_handle);
}
if (m_toggle_thread.joinable())
{
m_toggle_thread.join();
m_editor_listener_thread.join();
}
}
@@ -584,22 +500,17 @@ public:
return false;
}
constexpr ULONGLONG hotkeyToggleDebounceMs = 500;
constexpr ULONGLONG hotkeyDebounceMs = 500;
const auto now = GetTickCount64();
if (now - m_lastHotkeyToggleTime < hotkeyToggleDebounceMs)
if (now - m_lastHotkeyTime < hotkeyDebounceMs)
{
return true;
}
m_lastHotkeyToggleTime = now;
m_lastHotkeyTime = now;
if (hotkeyId == 0)
{
// Toggle engine on/off
toggle_engine();
}
else if (hotkeyId == 1)
{
// Open the new editor (only in new editor mode)
// Open the editor
launch_editor();
}

View File

@@ -376,6 +376,7 @@ namespace PowerAccent.Core
LetterKey.VK_A => new[] { "á", "æ" },
LetterKey.VK_D => new[] { "ð" },
LetterKey.VK_E => new[] { "é" },
LetterKey.VK_I => new[] { "í" },
LetterKey.VK_O => new[] { "ó", "ö" },
LetterKey.VK_U => new[] { "ú" },
LetterKey.VK_Y => new[] { "ý" },

View File

@@ -11,6 +11,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class AlwaysOnTopProperties
{
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0x54);
public static readonly HotkeySettings DefaultIncreaseOpacityHotkeyValue = new HotkeySettings(true, true, false, false, 0xBB);
public static readonly HotkeySettings DefaultDecreaseOpacityHotkeyValue = new HotkeySettings(true, true, false, false, 0xBD);
public const bool DefaultFrameEnabled = true;
public const bool DefaultShowInSystemMenu = false;
public const int DefaultFrameThickness = 15;
@@ -24,6 +26,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public AlwaysOnTopProperties()
{
Hotkey = new KeyboardKeysProperty(DefaultHotkeyValue);
IncreaseOpacityHotkey = new KeyboardKeysProperty(DefaultIncreaseOpacityHotkeyValue);
DecreaseOpacityHotkey = new KeyboardKeysProperty(DefaultDecreaseOpacityHotkeyValue);
ShowInSystemMenu = new BoolProperty(DefaultShowInSystemMenu);
FrameEnabled = new BoolProperty(DefaultFrameEnabled);
FrameThickness = new IntProperty(DefaultFrameThickness);
@@ -39,6 +43,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("hotkey")]
public KeyboardKeysProperty Hotkey { get; set; }
[JsonPropertyName("increase-opacity-hotkey")]
public KeyboardKeysProperty IncreaseOpacityHotkey { get; set; }
[JsonPropertyName("decrease-opacity-hotkey")]
public KeyboardKeysProperty DecreaseOpacityHotkey { get; set; }
[JsonPropertyName("frame-enabled")]
public BoolProperty FrameEnabled { get; set; }

View File

@@ -40,6 +40,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
() => Properties.Hotkey.Value,
value => Properties.Hotkey.Value = value ?? AlwaysOnTopProperties.DefaultHotkeyValue,
"AlwaysOnTop_ActivationShortcut"),
new HotkeyAccessor(
() => Properties.IncreaseOpacityHotkey.Value,
value => Properties.IncreaseOpacityHotkey.Value = value ?? AlwaysOnTopProperties.DefaultIncreaseOpacityHotkeyValue,
"AlwaysOnTop_IncreaseOpacityShortcut"),
new HotkeyAccessor(
() => Properties.DecreaseOpacityHotkey.Value,
value => Properties.DecreaseOpacityHotkey.Value = value ?? AlwaysOnTopProperties.DefaultDecreaseOpacityHotkeyValue,
"AlwaysOnTop_DecreaseOpacityShortcut"),
};
return hotkeyAccessors.ToArray();

View File

@@ -21,20 +21,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnoreAttribute]
public GenericProperty<List<string>> KeyboardConfigurations { get; set; }
public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B);
public HotkeySettings DefaultEditorShortcut => new HotkeySettings(true, false, false, true, 0x51);
public KeyboardManagerProperties()
{
ToggleShortcut = DefaultToggleShortcut;
EditorShortcut = DefaultEditorShortcut;
KeyboardConfigurations = new GenericProperty<List<string>>(new List<string> { "default", });
ActiveConfiguration = new GenericProperty<string>("default");
}
public HotkeySettings ToggleShortcut { get; set; }
public HotkeySettings EditorShortcut { get; set; }
[JsonPropertyName("useNewEditor")]

View File

@@ -38,10 +38,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ToggleShortcut,
value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut,
"Toggle_Shortcut"),
new HotkeyAccessor(
() => Properties.EditorShortcut,
value => Properties.EditorShortcut = value ?? Properties.DefaultEditorShortcut,

View File

@@ -28,6 +28,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnore]
public static HotkeySettings DefaultSnipToggleKey => new HotkeySettings(false, true, false, false, '6'); // Ctrl+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 DefaultBreakTimerKey => new HotkeySettings(false, true, false, false, '3'); // Ctrl+3
@@ -44,6 +50,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public KeyboardKeysProperty SnipToggleKey { get; set; }
public KeyboardKeysProperty SnipOcrToggleKey { get; set; }
public KeyboardKeysProperty SnipPanoramaToggleKey { get; set; }
public KeyboardKeysProperty BreakTimerKey { get; set; }
public StringProperty Font { get; set; }
@@ -96,5 +106,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public BoolProperty MicMonoMix { get; set; }
public StringProperty MicrophoneDeviceId { get; set; }
public BoolProperty BreakLockWorkstation { get; set; }
}
}

View File

@@ -209,7 +209,7 @@ namespace Microsoft.PowerToys.Settings.UI
private void OpenSettingsItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
App.OpenSettingsWindow();
App.OpenSettingsWindow(ensurePageIsSelected: true);
}
}
}

View File

@@ -34,13 +34,13 @@
IsExpanded="True">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.Hotkey, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard>
<tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Vertical">
<tkcontrols:MarkdownTextBlock Config="{StaticResource DescriptionTextMarkdownConfig}" Text="{x:Bind ViewModel.IncreaseOpacityShortcut, Mode=OneWay}" />
<tkcontrols:MarkdownTextBlock Config="{StaticResource DescriptionTextMarkdownConfig}" Text="{x:Bind ViewModel.DecreaseOpacityShortcut, Mode=OneWay}" />
</StackPanel>
</tkcontrols:SettingsCard.Description>
<!-- HACK: For some weird reason, a ShortcutControl does not work correctly if it's the first or last item in the expander, so we add an invisible card. -->
<tkcontrols:SettingsCard Visibility="Collapsed" />
<tkcontrols:SettingsCard x:Uid="AlwaysOnTop_IncreaseOpacityShortcut">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.IncreaseOpacityHotkey, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="AlwaysOnTop_DecreaseOpacityShortcut">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.DecreaseOpacityHotkey, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="AlwaysOnTop_GameMode" IsChecked="{x:Bind ViewModel.DoNotActivateOnGameMode, Mode=TwoWay}" />

View File

@@ -62,28 +62,12 @@
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsExpander
<tkcontrols:SettingsCard
Name="KeyboardManagerEnableToggle"
x:Uid="KeyboardManager_EnableToggle"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/KeyboardManager.png}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.Enabled, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Description>
<HyperlinkButton NavigateUri="https://aka.ms/powerToysCannotRemapKeys">
<TextBlock x:Uid="KBM_KeysCannotBeRemapped" FontWeight="SemiBold" />
</HyperlinkButton>
</tkcontrols:SettingsExpander.Description>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard
Name="ToggleShortcut"
x:Uid="KeyboardManager_Toggle_Shortcut"
IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AllowDisable="True"
HotkeySettings="{x:Bind Path=ViewModel.ToggleShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<tkcontrols:SwitchPresenter TargetType="x:Boolean" Value="{x:Bind ViewModel.UseNewEditor, Mode=OneWay}">

View File

@@ -240,6 +240,9 @@
Visibility="{x:Bind ViewModel.BreakShowBackgroundFile, Mode=OneWay}">
<CheckBox x:Uid="ZoomIt_Break_BackgroundStretch" IsChecked="{x:Bind ViewModel.BreakBackgroundStretch, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItBreakLockWorkstation" ContentAlignment="Left">
<CheckBox x:Uid="ZoomIt_Break_LockWorkstation" IsChecked="{x:Bind ViewModel.BreakLockWorkstation, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard>
<tkcontrols:SettingsCard.Description>
<tkcontrols:MarkdownTextBlock x:Uid="ZoomIt_BreakFAQ" Config="{StaticResource DescriptionTextMarkdownConfig}" />
@@ -325,6 +328,22 @@
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
Name="ZoomItSnipOcrShortcut"
x:Uid="ZoomIt_SnipOcr_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xF7ED;}"
IsExpanded="True">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind ViewModel.SnipOcrToggleKey, Mode=TwoWay}" />
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="ZoomIt_PanoramaGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
Name="ZoomItPanoramaShortcut"
x:Uid="ZoomIt_Panorama_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xE7C5;}"
IsExpanded="True">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind ViewModel.SnipPanoramaToggleKey, Mode=TwoWay}" />
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>

View File

@@ -1909,12 +1909,6 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
</data>
<data name="Activation_Shortcut.Description" xml:space="preserve">
<value>Customize the shortcut to activate this module</value>
</data>
<data name="KeyboardManager_Toggle_Shortcut.Header" xml:space="preserve">
<value>Shortcut</value>
</data>
<data name="KeyboardManager_Toggle_Shortcut.Description" xml:space="preserve">
<value>Enable or disable this module (Note: the Settings UI will not update)</value>
</data>
<data name="KeyboardManager_Editor_Shortcut.Header" xml:space="preserve">
<value>Editor shortcut</value>
@@ -3054,11 +3048,17 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="AlwaysOnTop_ActivationShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to pin or unpin an app window</value>
</data>
<data name="AlwaysOnTop_IncreaseOpacity" xml:space="preserve">
<value>Press **{0}** to increase the opacity of the window</value>
<data name="AlwaysOnTop_IncreaseOpacityShortcut.Header" xml:space="preserve">
<value>Increase opacity</value>
</data>
<data name="AlwaysOnTop_DecreaseOpacity" xml:space="preserve">
<value>Press **{0}** to decrease the opacity of the window</value>
<data name="AlwaysOnTop_IncreaseOpacityShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to increase the opacity of a pinned window</value>
</data>
<data name="AlwaysOnTop_DecreaseOpacityShortcut.Header" xml:space="preserve">
<value>Decrease opacity</value>
</data>
<data name="AlwaysOnTop_DecreaseOpacityShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to decrease the opacity of a pinned window</value>
</data>
<data name="Oobe_AlwaysOnTop.Title" xml:space="preserve">
<value>Always On Top</value>
@@ -4807,6 +4807,9 @@ The break timer font matches the text font.</value>
<data name="ZoomIt_Record_Microphones_Default_Name" xml:space="preserve">
<value>Default</value>
</data>
<data name="ZoomIt_Break_LockWorkstation.Content" xml:space="preserve">
<value>Lock workstation during break</value>
</data>
<data name="ZoomIt_SnipGroup.Header" xml:space="preserve">
<value>Snip</value>
</data>
@@ -4819,6 +4822,24 @@ The break timer font matches the text font.</value>
<data name="ZoomIt_Snip_Shortcut_Save" xml:space="preserve">
<value>Press **{0}** to save the snip to a file instead of the clipboard.</value>
</data>
<data name="ZoomIt_SnipOcr_Shortcut.Header" xml:space="preserve">
<value>Snip OCR activation</value>
</data>
<data name="ZoomIt_SnipOcr_Shortcut.Description" xml:space="preserve">
<value>Copy text from the selected region to the clipboard.</value>
</data>
<data name="ZoomIt_PanoramaGroup.Header" xml:space="preserve">
<value>Panorama</value>
</data>
<data name="ZoomIt_PanoramaGroup.Description" xml:space="preserve">
<value>Capture a scrolling panorama of a selected screen region.</value>
</data>
<data name="ZoomIt_Panorama_Shortcut.Header" xml:space="preserve">
<value>Panorama activation</value>
</data>
<data name="ZoomIt_Panorama_Shortcut.Description" xml:space="preserve">
<value>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.</value>
</data>
<data name="Oobe_ZoomIt.Description" xml:space="preserve">
<value>ZoomIt is a screen zoom, annotation, and recording tool for technical presentations and demos. You can also use ZoomIt to snip screenshots to the clipboard or to a file.</value>
<comment>{Locked="ZoomIt"}</comment>
@@ -5877,7 +5898,7 @@ Text uses the current drawing color.</value>
<value>Welcome to PowerToys</value>
</data>
<data name="ShortcutConflictWindow_IgnoreShortcut.Content" xml:space="preserve">
<value>Ignore shortcut</value>
<value>Ignore conflict</value>
</data>
<data name="AdvancedPaste_AddModelButton.Content" xml:space="preserve">
<value>Add model</value>

View File

@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using global::PowerToys.GPOWrapper;
@@ -50,6 +49,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings = moduleSettingsRepository.SettingsConfig;
_hotkey = Settings.Properties.Hotkey.Value;
_increaseOpacityHotkey = Settings.Properties.IncreaseOpacityHotkey.Value;
_decreaseOpacityHotkey = Settings.Properties.DecreaseOpacityHotkey.Value;
_showInSystemMenu = Settings.Properties.ShowInSystemMenu.Value;
_frameEnabled = Settings.Properties.FrameEnabled.Value;
_frameThickness = Settings.Properties.FrameThickness.Value;
@@ -85,7 +86,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [Hotkey],
[ModuleName] = [Hotkey, IncreaseOpacityHotkey, DecreaseOpacityHotkey],
};
return hotkeysDict;
@@ -135,10 +136,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged();
// Also notify that transparency shortcut strings have changed
OnPropertyChanged(nameof(IncreaseOpacityShortcut));
OnPropertyChanged(nameof(DecreaseOpacityShortcut));
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
@@ -150,6 +147,52 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public HotkeySettings IncreaseOpacityHotkey
{
get => _increaseOpacityHotkey;
set
{
if (value != _increaseOpacityHotkey)
{
_increaseOpacityHotkey = value ?? AlwaysOnTopProperties.DefaultIncreaseOpacityHotkeyValue;
Settings.Properties.IncreaseOpacityHotkey.Value = _increaseOpacityHotkey;
NotifyPropertyChanged();
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
AlwaysOnTopSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.AlwaysOnTopSettings)));
}
}
}
public HotkeySettings DecreaseOpacityHotkey
{
get => _decreaseOpacityHotkey;
set
{
if (value != _decreaseOpacityHotkey)
{
_decreaseOpacityHotkey = value ?? AlwaysOnTopProperties.DefaultDecreaseOpacityHotkeyValue;
Settings.Properties.DecreaseOpacityHotkey.Value = _decreaseOpacityHotkey;
NotifyPropertyChanged();
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
AlwaysOnTopSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.AlwaysOnTopSettings)));
}
}
}
public bool FrameEnabled
{
get => _frameEnabled;
@@ -310,32 +353,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
/// <summary>
/// Gets the formatted shortcut string for increasing window opacity (modifier keys + "+").
/// </summary>
public string IncreaseOpacityShortcut
{
get
{
var modifiers = new HotkeySettings(_hotkey.Win, _hotkey.Ctrl, _hotkey.Alt, _hotkey.Shift, 0).ToString();
var shortcut = string.IsNullOrEmpty(modifiers) ? "+" : modifiers + " + +";
return string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("AlwaysOnTop_IncreaseOpacity"), shortcut);
}
}
/// <summary>
/// Gets the formatted shortcut string for decreasing window opacity (modifier keys + "-").
/// </summary>
public string DecreaseOpacityShortcut
{
get
{
var modifiers = new HotkeySettings(_hotkey.Win, _hotkey.Ctrl, _hotkey.Alt, _hotkey.Shift, 0).ToString();
var shortcut = string.IsNullOrEmpty(modifiers) ? "-" : modifiers + " + -";
return string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("AlwaysOnTop_DecreaseOpacity"), shortcut);
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
@@ -352,6 +369,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _enabledStateIsGPOConfigured;
private bool _isEnabled;
private HotkeySettings _hotkey;
private HotkeySettings _increaseOpacityHotkey;
private HotkeySettings _decreaseOpacityHotkey;
private bool _showInSystemMenu;
private bool _frameEnabled;
private int _frameThickness;

View File

@@ -521,7 +521,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
ISettingsRepository<AlwaysOnTopSettings> moduleSettingsRepository = SettingsRepository<AlwaysOnTopSettings>.GetInstance(SettingsUtils.Default);
var list = new List<DashboardModuleItem>
{
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AlwaysOnTop_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.Hotkey.Value.GetKeysList() },
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AlwaysOnTop_ActivationShortcut/Header"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.Hotkey.Value.GetKeysList() },
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AlwaysOnTop_IncreaseOpacityShortcut/Header"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.IncreaseOpacityHotkey.Value.GetKeysList() },
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AlwaysOnTop_DecreaseOpacityShortcut/Header"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.DecreaseOpacityHotkey.Value.GetKeysList() },
};
return new ObservableCollection<DashboardModuleItem>(list);
}

View File

@@ -181,34 +181,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [ToggleShortcut, EditorShortcut],
[ModuleName] = [EditorShortcut],
};
return hotkeysDict;
}
public HotkeySettings ToggleShortcut
{
get => Settings.Properties.ToggleShortcut;
set
{
if (value != Settings.Properties.ToggleShortcut)
{
Settings.Properties.ToggleShortcut = value == null ? Settings.Properties.DefaultToggleShortcut : value;
OnPropertyChanged(nameof(ToggleShortcut));
NotifySettingsChanged();
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
KeyboardManagerSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.KeyboardManagerSettings)));
}
}
}
public bool UseNewEditor
{
get => Settings.Properties.UseNewEditor;

View File

@@ -367,6 +367,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public HotkeySettings SnipOcrToggleKey
{
get => _zoomItSettings.Properties.SnipOcrToggleKey.Value;
set
{
if (_zoomItSettings.Properties.SnipOcrToggleKey.Value != value)
{
_zoomItSettings.Properties.SnipOcrToggleKey.Value = value ?? ZoomItProperties.DefaultSnipOcrToggleKey;
OnPropertyChanged(nameof(SnipOcrToggleKey));
NotifySettingsChanged();
}
}
}
public HotkeySettings SnipPanoramaToggleKey
{
get => _zoomItSettings.Properties.SnipPanoramaToggleKey.Value;
set
{
if (_zoomItSettings.Properties.SnipPanoramaToggleKey.Value != value)
{
_zoomItSettings.Properties.SnipPanoramaToggleKey.Value = value ?? ZoomItProperties.DefaultSnipPanoramaToggleKey;
OnPropertyChanged(nameof(SnipPanoramaToggleKey));
NotifySettingsChanged();
}
}
}
public HotkeySettings BreakTimerKey
{
get => _zoomItSettings.Properties.BreakTimerKey.Value;
@@ -783,6 +811,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool BreakLockWorkstation
{
get => _zoomItSettings.Properties.BreakLockWorkstation.Value;
set
{
if (_zoomItSettings.Properties.BreakLockWorkstation.Value != value)
{
_zoomItSettings.Properties.BreakLockWorkstation.Value = value;
OnPropertyChanged(nameof(BreakLockWorkstation));
NotifySettingsChanged();
}
}
}
public double RecordScaling
{
get