Compare commits

...

48 Commits

Author SHA1 Message Date
Niels Laute
96f89906e4 Small settings UI tweaks 2025-09-05 16:09:55 +02:00
Mike Hall
7697876174 add settle time and progress arc 2025-09-05 13:47:47 +01:00
Mike Hall
6593ec69d5 add dwell cursor functionality 2025-09-04 15:57:33 +01:00
Jiří Polášek
f760ed9d34 CmdPal: Add option to Clipboard History extension to keep item after pasting (#41444)
<!-- 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 toggle in the Clipboard History extension
settings that allows items to remain in history after being pasted into
another application.

It also adds a separate menu item to remove items from history manually.
Item deletion is protected by a confirmation prompt, which can be
disabled in the settings.

Additionally, it introduces a shared `ConfirmableCommand` that can wrap
any command with a confirmation prompt.

Pictures? Pictures!

<img width="1541" height="981" alt="image"
src="https://github.com/user-attachments/assets/ed046f6e-f2dd-494c-b393-36add6b77346"
/>

<img width="1482" height="930" alt="image"
src="https://github.com/user-attachments/assets/fea89e55-ade0-4b6d-8fe2-d9a2b861bb49"
/>

<img width="1526" height="948" alt="image"
src="https://github.com/user-attachments/assets/a1041ce8-ae44-4b1f-8ed4-ec464580092a"
/>



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

- [x] Closes: #41433
- [ ] **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
2025-09-03 15:37:38 -05:00
Jiří Polášek
7d8f64cf3c CmdPal: Update WebSearch extension history immediately (#41398)
## Summary of the Pull Request

This PR ensures that the list of recent searches in the Web Search
extension is updated immediately after a new item is added or when
settings controlling the number of items are changed.

- Refactors the Web Search extension history to keep it in memory after
being loaded at startup
- Adds an event to notify subscribers when the history changes  
- Implements `IDisposable` to ensure that `WebSearchListPage`
unsubscribes from the event
- Moves responsibility for creating all list items to single class
(`WebSearchListPage`)
- Updated unit tests
- 
## PR Checklist

- [x] Closes: #40548
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** nothing
- [x] **New binaries:** none
- [x] **Documentation updated:** nope

<!-- 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
2025-09-03 13:47:33 -05:00
Jiří Polášek
347c3f1efa CmdPal: Enhance font icon classification and visuals (#41573)
## Summary of the Pull Request

- Introduces `FontIconGlyphClassifier` for classifying emojis and
symbols.
- Correctly recognizes multi-codepoint glyphs (e.g., 🧙🏼‍♀️ *woman mage
with medium-light skin tone*).
- Explicitly disallows multi-glyph icons (they would overflow anyway).
- Distinguishes between emojis and regular text characters (letters,
numbers, symbols), since emojis are slightly larger and require
different padding.
- Recognizes Unicode [Variation
Selectors](https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block))
to enforce specific styles: VS15 (U+FE0E) for text style (monochrome)
and VS16 (U+FE0F) for emoji style (color). This lets developers choose
which variant to display. By default, characters with both
representations render as text/monochrome (e.g., ▶ `\u25B6`):
<img width="428" height="39" alt="image"
src="https://github.com/user-attachments/assets/c5e6865f-61de-4f45-9f3a-4e15e5e5ceb8"
/>
- Invalid icons are displayed as a dashed circle so extension developers
can spot issues, without being overly distracting if they slip into
production.

- Updates `IconPathConverter` to use the new classifier for improved
icon handling.
- Adds `SampleIconPage` to demonstrate various icon usages and
classifications.
- Adjusts icon alignment in `IconBox` so icons are centered.  
- Scales negative padding for emojis in `IconBox` with control size,
fixing misalignment and clipping (noticeable in tags and the details
pane hero image).
- Applies negative padding to all font icons. This removes the need for
classification in these cases and ensures symbols rendered below the
baseline remain visible.

Based on
[microsoft/terminal#19143](https://github.com/microsoft/terminal/pull/19143):
Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>

Pictures? Pictures!

<img width="1912" height="2394" alt="image"
src="https://github.com/user-attachments/assets/05a16309-b658-4f21-8f9d-9a3f20db6ad8"
/>

Keyboard and flag/country emojis may look a bit off, but that’s how
they’re actually rendered:
<img width="482" height="95" alt="image"
src="https://github.com/user-attachments/assets/dc7d4d0d-3dc8-4df5-9b9f-9e977e7e989f"
/>


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

- [x] Closes: 
  - #41489 
  - #41496 
- [ ] **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
2025-09-03 13:17:52 -05:00
Kayla Cinnamon
b71bbf89ce Fix the dedup workflow so it actually works (#41580)
<!-- 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
Had some bad syntax in the first iteration, tested this on my fork and
it adds the "duplicate" label and comments which issues it's a dup of.
<img width="952" height="333" alt="image"
src="https://github.com/user-attachments/assets/71645d79-5fa0-4c66-a560-32a033d14dc9"
/>


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

- [ ] Closes: #xxx
- [ ] **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
2025-09-03 17:25:43 +00:00
Jiří Polášek
caa7114e6f CmdPal: Add keyboard shortcuts to items in File Search extension (#41413)
## Summary of the Pull Request

Adds keyboard shortcuts to `IndexerListItem` commands to mirror those in
**All Apps**.

- Introduces a new `KeyChords` class that defines and manages key chords
for all actions within individual projects, improving code organization
and maintainability (similar to the `Icons` class).
- Adds a `WellKnownKeyChords` class in the shared project that defines
common shortcuts to be used consistently across the entire app.
- Updates `IndexerListItem` to include new command context items with
`RequestedShortcut` properties for:
  - Show in folder (`Ctrl+Shift+E`)  
  - Copy path (`Ctrl+Shift+C`)  
  - Open path in console (`Ctrl+Shift+R`)  
- Updates `AppListItem`, `UWPApplication`, and `Win32Program` to use the
new key chord properties from the `KeyChords` class, ensuring
consistency and maintainability.

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

- [ ] Closes: #xxx
- [ ] **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
2025-09-03 11:30:57 -05:00
leileizhang
09a1217026 Fixed a memory alignment issue that caused the measure tool to crash on some machines. (#41556)
<!-- 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
The EXCEPTION_DATATYPE_MISALIGNMENT error is a classic memory alignment
issue, and add alignas(8) to Ensures cursorPosSystemSpace is aligned on
an 8-byte boundary

This happened on the pipeline’s ARM64 UI test machine.

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

- [x] Closes: #41555
- [ ] **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
2025-09-03 13:56:51 +08:00
Jiří Polášek
3f6b5e4a65 CmdPal: Respect Ignore shortcut in fullscreen mode when using the low-level keyboard hook (#41402)
<!-- 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

Refactors the logic that determines whether to ignore keyboard shortcuts
while in fullscreen mode. The check is now handled in `HandleSummon`,
which centralizes all precondition checks before delegating the actual
summoning to `HandleSummonCore`.

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

- [x] Closes: #41265
- [ ] **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
2025-09-02 21:11:16 -05:00
Jessica Dene Earley-Cha
d85ccb9c58 [CmdPal] Restore focus to More button after context menu closes via escape button (#41364)
## Summary of the Pull Request

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

- [x] Closes: #41363
- [ ] **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/374fd1bc-8e62-4117-a613-f0d35678e3ed
2025-09-02 19:53:53 -05:00
Michael Knauer
7d70e6e73f DevDocs: Add prerequisites to enable long paths in Windows (#41329)
<!-- 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 the "long paths" to the prerequisites in the DevDocs

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Dev docs:** Added/updated

<!-- 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
While setting up the PowerToys solution, I ran into repeated issues
where files or SDKs could not be found when loading the solution. After
a full evening of debugging, the root cause turned out to be Windows’
default path length limitation.

Even with a repository path like C:/Users/Micha/Development/PowerToys
(which is not unusually long by itself), the combination of folder
structure and file names exceeded the maximum allowed path length and
caused the build problems.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-09-02 19:43:28 -05:00
Jiří Polášek
57151cb8cd CmdPal: Fix breadcrumb misalignment in Settings window [nit] (#41455)
## Summary of the Pull Request

Fixes margin of the breadcrumb control in the Settings window:

<img width="3840" height="2160" alt="dasdasdasdasd"
src="https://github.com/user-attachments/assets/d70af259-cb77-4d67-8b0d-8c7870bb8774"
/>


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

- [ ] Closes: #xxx
- [ ] **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
2025-09-02 15:42:25 -05:00
Jiří Polášek
d153f3473a CmdPal: Improve error handling and logging in activation process (#41344)
## Summary of the Pull Request

This PR makes passing arguments from a new instance to the existing one
more resilient:


- Fixes situations where `x-cmdpal://` links might not work as expected.
Instead of performing the intended action (e.g., `x-cmdpal://background`
or `x-cmdpal://settings`), they could incorrectly just summon the main
window.

- Refactors the `AppInstance.Activated` handler to be synchronous.  
- The handler blocks `AppInstance.RedirectActivationToAsync` in the
caller.
- If it runs asynchronously (or offloads work to another thread,
including the UI thread), the calling instance may exit too soon,
preventing the activation arguments from being read.

- Adds a timeout and ensures the semaphore is always released so the
application can exit gracefully under all conditions.

- Adjusts handling for cases where the source application exits before
passing arguments by lowering the log severity to **Warning** and
providing a clearer, more descriptive message.

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

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** yop
- [x] **Localization:** no need
- [x] **Dev docs:** no need
- [x] **New binaries:** none
- [x] **Documentation updated:** nope

<!-- 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
Tested with x-cmdpal://settings under normal conditions, and with CmdPal
deliberately slowed down to take its sweet time handling the arguments
(so the calling instance times out).
2025-09-02 15:39:59 -05:00
Jiří Polášek
7931f14bd5 CmdPal: Prevent cloaking of a visible window when not necessary (#40981)
## Summary of the Pull Request

This change prevents unnecessary cloaking of a visible window when
showing it to the user, significantly reducing flickering. Some minor
flickering remains due to page content refreshes, but it is much less
noticeable.

New windows are now pre-cloaked immediately after creation. Cloaking is
explicitly applied only when an animation is expected (e.g., when
minimized).

Additionally, this change removes explicit window activation, as the
summon mechanism will activate the window as needed.


## PR Checklist

- [x] Closes: #40969 
- [ ] **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
- [x] **Localization:** no need
- [x] **Dev docs:** none
- [x] **New binaries:** none
- [x] **Documentation updated:** none

## Detailed Description of the Pull Request / Additional comments

## Validation Steps Performed
2025-09-02 15:39:27 -05:00
Jiří Polášek
30cf16c302 CmdPal: Using TitleBar (#41542)
## Summary of the Pull Request

Using TitleBar instead of custom XAML

<img width="1379" height="764" alt="image"
src="https://github.com/user-attachments/assets/cf2d4516-1566-4c3d-a4cf-e371d1ebe16d"
/>


## PR Checklist

- [x] Closes: #41414
- [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
2025-09-02 15:31:58 -05:00
Kayla Cinnamon
3729fe912e Update action-genai-issue-dedup version to v0 (#41547)
<!-- 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
Turns out this is supposed to be v0, not v1

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

- [ ] Closes: #xxx
- [ ] **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
2025-09-02 20:16:07 +00:00
Kayla Cinnamon
d777e61c6f Add duplicate issue detection as GitHub Action (#41546)
<!-- 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
Trying out GitHub models to batch detect duplicate issues.

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

- [ ] Closes: #xxx
- [ ] **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
2025-09-02 20:01:09 +00:00
Jiří Polášek
0f0a3f155a CmdPal: Make LogError include HRESULT in the log (#41393)
## Summary of the Pull Request

Enhances `Logger.LogError` to log the exception HRESULT in addition to
the exception type and message, improving traceability and debugging
efficiency. For ~bug~ fun and profit!

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

- [ ] Closes: #xxx
- [ ] **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
2025-09-02 12:21:43 -05:00
Jiří Polášek
4e7871b0bf CmdPal: Handle corrupted app state file when saving new state (#41422)
## Summary of the Pull Request

Ensures the new state is saved even if the previous one is corrupted,
which is then ignored. Improves error handling and code clarity in
`AppStateModel`. Replaces Debug sink logging with structured Logger.

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

- [x] Closes: #41421 
- [ ] **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
2025-09-02 12:21:14 -05:00
Jiří Polášek
74b6140911 CmdPal: Make sure fallback items have icons visible in Settings window (#41427)
## Summary of the Pull Request

This PR ensures that fallback items consistently display an icon in
Settings window:

- Adds a new `InitialIcon` property to `TopLevelViewModel` to store the
first non-empty icon received.
- Uses `InitialIcon` in the extension settings page when listing
fallback items belonging to an extension.
- Sets initial icons in the constructor for fallback items that were not
previously initialized:
  - Date & Time extension  
  - System Commands extension
- The Windows Settings extension had its icon initially set, but it was
cleared when the item was updated for an empty search query. By
persisting the initial icon, subsequent updates no longer affect how the
fallback item is represented in Settings.

This change is considered a hotfix for the current state.  
The ideal long-term solution would be to declare the `DisplayIcon` on
fallback item explicitly, similar to `DisplayTitle`.
 
Pictures!

Date and Time:
<img width="495" height="218" alt="image"
src="https://github.com/user-attachments/assets/0f5815ed-62ce-4479-9bb9-692a1b8dbaa6"
/>

Windows Settings extension:
<img width="429" height="209" alt="image"
src="https://github.com/user-attachments/assets/03b5bc6e-6ef0-4f0f-8d9f-d71c0df1f49d"
/>

System Commands extension
<img width="632" height="426" alt="image"
src="https://github.com/user-attachments/assets/63ae2486-8e60-462c-84c6-ad914826efec"
/>


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

- [x] Closes: #41404
- [ ] **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

- Verified that icons are correctly displayed for all built-in
extensions.
- Confirmed correct behavior with third-party extensions (e.g., Colors
for Command Palette).
- Tested in Release configuration with AOT enabled.
2025-09-02 12:20:59 -05:00
leileizhang
fb6f620f9d Initial draft for 0.94 release note (#41439)
This pull request updates the `README.md` to document the PowerToys 0.94
release. It includes comprehensive updates to release notes, download
links, and roadmap details, reflecting new features, improvements, and
fixes across multiple modules. The changes also improve documentation
for developers and update future planning information.

Release documentation and download updates:
* Updates all download links, release references, and installer
filenames from version 0.93 to 0.94 throughout the `README.md` to ensure
users get the latest release.
* Revises the release notes to highlight major new features (like
installer upgrade to WiX 5, settings search, hotkey conflict detection,
and module-specific enhancements), and reorganizes the changelog for
clarity.
* Updates the roadmap and "What's Next" section to reflect plans for
version 0.95 and beyond, including new features and ongoing work.

Module and feature highlights:
* Adds or expands sections for Always On Top, Hosts File Editor, Image
Resizer, Mouse Without Borders, and PowerRename, summarizing key
improvements and new options in each.
* Details significant Command Palette improvements, including
accessibility, stability, UI tweaks, and extension updates.

Developer and documentation improvements:
* Adds new documentation for building and testing the installer, fixes
broken links, and updates developer guidance for building modules and
using the test suite.
* Summarizes development process improvements, such as updated
dependencies, improved test coverage, and CI enhancements.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-09-02 17:31:42 +02:00
Mike Griese
7fb2ff5119 CmdPal: init Details in a slow pass; add more winget logs (#41425)
Related to #41384

We should load the `IDetails` from a `IListItem` in the slow pass,
instead of immediately when we load the list of items.

see also #39215

Also adds a lot of logging on our side, which helped ID that it isn't
our fault that the winget APIs are returning slowly. That's tracked
upstream (somewhere)
2025-09-02 06:20:56 -05:00
Jiří Polášek
95b19739f6 CmdPal: Replace localized strings used as setting values in WebSearch extension with literals (#41331)
## Summary of the Pull Request

For WebSearch extension:

- Replaces localized string identifiers with invariant literal keys to
ensure stable and consistent setting values, avoiding issues when
switching cultures or if display strings change.
- Renames the `ShowHistory` property to `HistoryItemCount`.  
- Changes the type from `string` to `int` and centralizes parsing logic
in `SettingsManager`.
- Retains backward compatibility by preserving the legacy settings key
`"ShowHistory"` in `SettingsManager`.


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

- [x] Closes: #40547 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** none
- [x] **New binaries:** none
- [x] **Documentation updated:** none

<!-- 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
2025-09-02 06:16:54 -05:00
Jiří Polášek
c5c6a3658f CmdPal: Sync generated resource comment in with .resx value [nit] (#41476)
## Summary of the Pull Request

Only the .resx change (None → Unlimited) was previously committed,
leaving the generated strongly-typed resource class out of sync. Visual
Studio kept regenerating the designer with a comment change on every
run. This commit updates the comment in the generated file to match the
source value, eliminating the noisy diffs.

Ref: #40915

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

- [ ] Closes: #xxx
- [ ] **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
2025-09-02 05:44:44 -05:00
Niels Laute
549d30892d [Hosts File Editor] Using TitleBar (#41416)
Using `TitleBar` instead of our custom XAML.

No visual changes:

<img width="779" height="301" alt="image"
src="https://github.com/user-attachments/assets/ada36442-d2f4-4859-8b6e-c5581ae34ad2"
/>


- [X] Closes: #41414
- [ ] **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
2025-09-02 07:02:43 +02:00
Niels Laute
566e35af1e [Registry Preview] Using TitleBar and AppWindow (#41418)
## Summary of the Pull Request
- Using TitleBar instead of custom XAML
- Using `AppWindow` (as part of WinUIEx)

<img width="1163" height="246" alt="image"
src="https://github.com/user-attachments/assets/65a65c3a-81b7-4afb-b046-57e081709e98"
/>


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

- [x] Closes: #41414
- [ ] **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: Jay <65828559+Jay-o-Way@users.noreply.github.com>
2025-09-01 15:25:11 +02:00
Niels Laute
4ced93ce67 [Environment Variables] Using TitleBar (#41417)
Using `TitleBar` instead of our custom XAML.

No visual changes:

<img width="1254" height="216" alt="image"
src="https://github.com/user-attachments/assets/3e5bed64-3abe-421e-9345-59ad5228c134"
/>


- [X] Closes: #41414
- [ ] **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
2025-09-01 15:24:48 +02:00
Gordon Lam
05ae867ac8 Enable just build in any cmd or powershell simply (not necessary in Developer Command Prompt for VS 2022) (#41475)
This pull request refactors and modernizes the PowerToys build scripts
to provide a more robust, platform-aware, and user-friendly local build
experience. The changes introduce shared PowerShell helpers, add clear
documentation, improve Visual Studio environment detection, and unify
build logic across scripts. The new approach enables easier builds from
any folder, better error reporting, and automatic platform detection for
x64/arm64.

**Build system modernization and shared helpers:**

* Added new shared helper script `build-common.ps1` containing reusable
functions for MSBuild invocation, Visual Studio environment
initialization, project discovery, and platform auto-detection. This
script is now dot-sourced by all build scripts for consistent behavior.
* Refactored `build-essentials.ps1` and `build-installer.ps1` to use the
shared helpers, enabling automatic Visual Studio dev environment setup
and platform detection. Both scripts now work from any folder inside the
repo and provide improved logging and error handling.
[[1]](diffhunk://#diff-946ed85e16779fdbcfeb7de80f631eae2da0f7bd478e27e22621121b409dde88L1-R73)
[[2]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L55-R77)

**Improved build workflow and error reporting:**

* All build scripts now write detailed logs (full, errors, warnings,
binlog) next to the solution/project being built, with troubleshooting
guidance documented in the new guidelines.
[[1]](diffhunk://#diff-43764921d6c830dbb3a15fe875aebfbc46966ae5ff62f3179adb3ff046b47b9dR1-R284)
[[2]](diffhunk://#diff-283bc775aac55085b6a0a47e40b3cf619fff47e20a2f5537fd6dd342d19d2afdR1-R48)
* Command-line wrappers (`build.cmd`, `build-essentials.cmd`) added for
easy invocation from `cmd.exe`, forwarding all arguments to the
PowerShell scripts.
[[1]](diffhunk://#diff-4bf353f2a88f1378983e4e2f3a5555e69b6a6ccfbe004001c1ebfe99ca57903dR1-R5)
[[2]](diffhunk://#diff-48b3da077cd89d8ed6befe57a781bea813e6f9594bfcefbc320b20dea589c5abR1-R6)

**Documentation and usage guidance:**

* Introduced `BUILD-GUIDELINES.md` with clear instructions, usage
examples, and troubleshooting tips for all build scripts, including
platform overrides and log locations.

**Installer pipeline improvements:**

* The installer build pipeline (`build-installer.ps1`) now uses shared
helpers for platform detection and Visual Studio initialization, and
passes unified build arguments to all MSBuild invocations, improving
consistency and maintainability.
[[1]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L83-L126)
[[2]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L137-R113)
[[3]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L151-R128)
[[4]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L162-R142)

---

**References:**  

[[1]](diffhunk://#diff-43764921d6c830dbb3a15fe875aebfbc46966ae5ff62f3179adb3ff046b47b9dR1-R284)
[[2]](diffhunk://#diff-946ed85e16779fdbcfeb7de80f631eae2da0f7bd478e27e22621121b409dde88L1-R73)
[[3]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L55-R77)
[[4]](diffhunk://#diff-283bc775aac55085b6a0a47e40b3cf619fff47e20a2f5537fd6dd342d19d2afdR1-R48)
[[5]](diffhunk://#diff-4bf353f2a88f1378983e4e2f3a5555e69b6a6ccfbe004001c1ebfe99ca57903dR1-R5)
[[6]](diffhunk://#diff-48b3da077cd89d8ed6befe57a781bea813e6f9594bfcefbc320b20dea589c5abR1-R6)
[[7]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L83-L126)
[[8]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L137-R113)
[[9]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L151-R128)
[[10]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L162-R142)
2025-09-01 10:23:27 +08:00
Niels Laute
377d134d40 [File Locksmith] Using TitleBar (#41419)
## Summary of the Pull Request

Using TitleBar instead of our custom XAML.

No visual changes:
<img width="1205" height="288" alt="image"
src="https://github.com/user-attachments/assets/a41cf33d-7af7-4f4e-88e5-07cc1c47f09d"
/>

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

- [X] Closes: #41414
- [ ] **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
2025-08-29 20:47:36 +02:00
Niels Laute
be160d93f5 [Settings] String updates (#41420)
## Summary of the Pull Request
- A few strings (on the ZoomIt page) were not using sentence casing.
- Header for the modules card was not localized.

## PR Checklist

- [ ] Closes: #xxx
- [ ] **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
2025-08-29 20:35:36 +02:00
Michael Knauer
0c45799bb5 CmdPal: Add "Uninstall Application" command (#41302)
<!-- 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 the ability to uninstall UWP apps directly from the Command
Palette (similar to the current Windows Start menu). For Win32
applications, the Windows Settings uninstall page is opened.

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

- [ ] Closes: #xxx
> Not existing
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
> I messaged this to @michaeljolley in his Twitch chat, and he said it
would be a cool feature. No further discussion has happened so far.
- [x] **Tests:** Added/updated and all pass
> No tests added, unsure which cases to cover
> A run of the existing tests for this Package passed 100%

- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
> Not required
- [ ] **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
I added a new file, `Commands/UninstallApplicationCommand.cs`, which
implements the logic to uninstall applications directly from the Command
Palette. The command differentiates between UWP apps and Win32 programs.
All common error scenarios are properly handled and logged to ensure
reliability and traceability.

Additionally, in `Icons.cs`, I included the "Delete" icon from the
Windows Start menu to be displayed alongside the uninstall commands in
the Command List, providing a familiar visual cue for users.

The uninstall commands have been integrated into the appropriate classes
for both UWP and Win32 applications, making them fully accessible and
consistent across the Command Palette.
The command can be triggered using the shortcut Ctrl + Shift + Delete
for quick access.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Tested locally; currently, verification was limited to uninstalling a
UWP application, unsure of additional test scenarios.

*Note: This is my first PR draft, so apologies if I missed anything.*

---------

Co-authored-by: KnauerM <michael.knauer@rheinbahn.de>
2025-08-29 09:20:22 -05:00
Mike Griese
077af2c74b CmdPal: Bump version to 0.5 (#41442)
title
2025-08-29 08:37:12 -05:00
Gordon Lam
a0ac7efd2d Improve how to build the project in PowerToys easier (#41459)
This pull request refactors and standardizes the PowerToys build scripts
to improve maintainability, reusability, and platform detection. The
main changes are the introduction of a shared helper script
(`build-common.ps1`), migration of build logic in individual scripts to
use these helpers, and enhanced platform auto-detection. This makes the
build pipeline more robust and easier to use from any directory within
the repository.

As the result of our recent changes, use the following guidance when
working in the PowerToys repo:

1. Use `build-essentials.ps1` before any development in general
- Purpose: restore NuGet packages for the full solution and build a
small set of essential native projects (runner, settings). This is a
fast way to ensure native artifacts required for local development are
available.

2. Use `build.ps1` from any folder
- Purpose: lightweight local builder. It auto-discovers the target
platform (x64/arm64/x86) and builds projects it finds in the current
directory.
- Notes: you can pass additional MSBuild arguments positionally (e.g.
`./tools/build/build.ps1 '/p:CIBuild=true'`) — the script will forward
them to MSBuild.
   - Use `-RestoreOnly` to only restore packages for local projects.

3. Use `build-installer.ps1` to create a local installer (use with
caution)
- Purpose: runs the full pipeline that restores, builds the full
solution, signs packages, and builds the installer (MSI/bootstrapper).
- Caution: this script performs cleaning (git clean) and installer
packaging steps that may remove untracked files under `installer/`.

Additional notes
- Shared helpers live in `build-common.ps1` and are used by the other
scripts (`RunMSBuild`, `RestoreThenBuild`, `BuildProjectsInDirectory`,
platform auto-detection).

**Shared build logic and helper functions:**

* Added new `tools/build/build-common.ps1` file containing reusable
PowerShell functions for MSBuild invocation, solution/project restore
and build, platform detection, and project discovery. All build scripts
now dot-source this file for shared functionality.

**Refactoring of build scripts to use shared helpers:**

* Updated `tools/build/build-essentials.ps1` to use `build-common.ps1`
helpers, including auto-detection of repository root and platform, and
simplified project build logic.
* Created new `tools/build/build.ps1` for quick local builds, using
shared helpers and supporting extra MSBuild arguments and platform
auto-detection.
* Refactored `tools/build/build-installer.ps1` to remove duplicate build
logic, use shared helpers, and support platform auto-detection and
argument forwarding.
[[1]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L55-R74)
[[2]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L83-L126)

**Improved platform detection and argument handling:**

* All scripts now auto-detect the target platform if not specified,
using the new `Get-DefaultPlatform` helper. This supports x64, arm64,
and x86 hosts.
[[1]](diffhunk://#diff-43764921d6c830dbb3a15fe875aebfbc46966ae5ff62f3179adb3ff046b47b9dR1-R166)
[[2]](diffhunk://#diff-946ed85e16779fdbcfeb7de80f631eae2da0f7bd478e27e22621121b409dde88L1-R70)
[[3]](diffhunk://#diff-7a444242b2a6d9c642341bd2ef45f51ba5698ad7827e5136e85eb483863967a7R1-R88)
[[4]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L55-R74)

**Consistent MSBuild invocation and logging:**

* MSBuild calls now consistently use shared helpers, centralized
logging, and support passing extra arguments such as `/p:CIBuild=true`
and custom solution/project paths.
[[1]](diffhunk://#diff-43764921d6c830dbb3a15fe875aebfbc46966ae5ff62f3179adb3ff046b47b9dR1-R166)
[[2]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L137-R110)
[[3]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L151-R125)
[[4]](diffhunk://#diff-21888769485d767c43c0895fe315e6f6d7384da62f60ef917d8a61a610da10b9L162-R139)

**Project and solution build improvements:**

* Build scripts now discover and build projects in preferred order
(.sln, .csproj, .vcxproj), and support restoring packages only if
requested.
[[1]](diffhunk://#diff-43764921d6c830dbb3a15fe875aebfbc46966ae5ff62f3179adb3ff046b47b9dR1-R166)
[[2]](diffhunk://#diff-7a444242b2a6d9c642341bd2ef45f51ba5698ad7827e5136e85eb483863967a7R1-R88)

Let me know if you need a walkthrough of the new helper functions or how
to use the updated build scripts!
2025-08-29 16:23:52 +08:00
leileizhang
3882db4479 [UI tests] Add accessibility IDs to settings navigation items and fix part of FancyZones issues (#41458)
<!-- 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 automation-friendly identifiers to navigation and
toggle UI elements throughout the PowerToys Settings. The main goal is
to improve UI test reliability and maintainability by using stable
`AutomationId` and `x:Name` attributes instead of relying on visible
text. It also updates the UI tests to use these new identifiers.

These changes make the UI automation more robust against localization
and UI text changes, improving test reliability and maintainability.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **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
2025-08-29 15:08:44 +08:00
Copilot
eb35b3a249 Fix grammatical error in Awake taskbar context menu: "1 hours" → "1 hour" (#41454)
This PR fixes a grammatical mistake in the PowerToys Awake taskbar
context menu where "1 hours" was displayed instead of the correct "1
hour".

## Problem
When right-clicking the Awake icon in the taskbar and hovering over
"Keep awake on interval", the menu incorrectly showed "1 hours" for the
one-hour option, which is grammatically incorrect in English.

![Before fix showing "1
hours"](https://github.com/user-attachments/assets/bd78b3b1-d076-4c84-8de0-bcd6d1ebefd8)

## Root Cause
The code always used the `AWAKE_HOURS` resource string (`"{0} hours"`)
regardless of the value, even when the count was 1. This resulted in
grammatically incorrect text like "1 hours".

## Solution
Added proper singular/plural handling by:

1. **Added new singular resources:**
   - `AWAKE_HOUR`: `"{0} hour"` for singular form
   - `AWAKE_MINUTE`: `"{0} minute"` for completeness and future-proofing

2. **Updated the logic in `Manager.cs`:**
- Modified `GetDefaultTrayOptions()` to use `AwakeHour` (singular) when
the value is 1
- Preserved existing behavior for all other values (30 minutes, 2 hours,
etc.)

3. **Generated corresponding code in `Resources.Designer.cs`** to expose
the new resource properties

## Impact
-  "1 hours" → "1 hour" (grammatically correct)
-  "2 hours" remains unchanged (still correct)
-  "30 minutes" behavior preserved
-  No breaking changes to existing functionality
-  Future-proofed for potential 1-minute custom intervals

The fix follows established patterns in the PowerToys codebase (similar
to `TimeRemainingConverter.cs` in ImageResizer) and makes minimal,
surgical changes to address only the reported issue.

Fixes #41220.

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `i1qvsblobprodcus353.vsblob.vsassets.io`
> - Triggering command: `dotnet build
src/modules/awake/Awake/Awake.csproj` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent)
(admins only)
>
> </details>



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

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-08-29 13:36:46 +08:00
Jiří Polášek
5daec13bc4 CmdPal | Bug Report Tool: Allow collection of Command Palette events from Windows Event Logs by Bug Report Tool (#41400)
## Summary of the Pull Request

Adds `Microsoft.CmdPal.UI.exe` to the list of processes, so Bug Report
Tool can pick Applications Logs for it when generating its report.

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

- [x] Closes: #41399
- [ ] **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
2025-08-28 12:48:30 -05:00
Kai Tao
ef6f4b2c3d Settings: Search fancy zone settings and swallow the ctrl+f event (#41437)
<!-- 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 primarily improves the maintainability and robustness
of the FancyZones settings UI by assigning unique `Name` attributes to
`SettingsCard` controls in the `FancyZonesPage.xaml` file. Additionally,
it enhances the logic for retrieving element UIDs and improves fallback
behavior for missing localizations. There is also a minor usability fix
in the shell page to prevent unintended navigation when focusing the
search box.

**FancyZones settings UI improvements:**

* Added unique `Name` attributes to all `tkcontrols:SettingsCard`
elements in `FancyZonesPage.xaml` to facilitate easier referencing and
future maintainability. This affects all relevant settings groups and
controls in the file.
[[1]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL73-R85)
[[2]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL109-R109)
[[3]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL121-R121)
[[4]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL167-R188)
[[5]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL202-R202)
[[6]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL251-R251)
[[7]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL265-R265)
[[8]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL280-R280)

**Element UID retrieval and localization:**

* Improved `GetElementUid` in `Program.cs` to fall back to the first
child's `x:Uid` if the element itself lacks one, increasing robustness
when parsing XAML.
* Updated `GetLocalizedSettingHeaderAndD` in `SearchIndexService.cs` to
provide a fallback for missing localizations by trying the
`"{elementUid}/Content"` resource key.

**Shell page usability:**

* Modified `CtrlF_Invoked` in `ShellPage.xaml.cs` to mark the event as
handled, preventing unintended navigation when the search box is focused
with Ctrl+F.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **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/9cf15605-1114-4c6d-923c-d05c2733a274
2025-08-28 11:18:58 +08:00
Michael Jolley
336cdaff9b CmdPal: Added settings for limiting apps on top level searches (#40915)
Closes #40062

Adds a setting to limit the number of apps returned on top level
searches.

Can limit to none, 1, 5, 10, or 20.


https://github.com/user-attachments/assets/de60111f-fb02-4db6-9ae9-2f636c171b5b
2025-08-27 10:52:39 -05:00
leileizhang
447118ab70 fix: Context menu registry entries are not cleaned up when disabled via GPO (#41411)
<!-- 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 fixes an issue where context menu runtime registration wasn't
properly cleaned up when GPO (Group Policy Object) policies disabled the
module. The problem occurred because the module constructor didn't
consider GPO policies when determining its initial enabled state.

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

- [x] Closes: #41387 
- [ ] **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

This pull request refactors how context menu registration and
unregistration are handled across several PowerToys modules. The main
improvement is the introduction of a new `UpdateRegistration` helper
method in each module, which centralizes and simplifies the logic for
registering or unregistering context menus when the module is enabled or
disabled. This reduces code duplication and ensures consistent behavior.

**Context menu registration logic refactor:**

* Added a private `UpdateRegistration` method to each of the following
modules to handle context menu registration and unregistration based on
the enabled state:
  - `FileLocksmithModule` in `PowerToysModule.cpp`
  - `NewModule` in `powertoys_module.cpp`
  - `ImageResizerModule` in `dllmain.cpp`
  - `PowerRenameModule` in `dllmain.cpp`

* Replaced direct calls to registration/unregistration functions in
`enable`, `disable`, and `init_settings` methods with calls to the new
`UpdateRegistration` method in all affected modules, ensuring consistent
and centralized handling
[[1]](diffhunk://#diff-256ed936dafec1bf6ff17849b4797dd276f5b07bebe2e483bc1580c8f06e92d9L91-R122)
[[2]](diffhunk://#diff-256ed936dafec1bf6ff17849b4797dd276f5b07bebe2e483bc1580c8f06e92d9R155)
[[3]](diffhunk://#diff-4a3942d548f3daec02a833983ed9b2b69f75e2cd1b74a8ce1b874f3fd33fde55L101-R125)
[[4]](diffhunk://#diff-4a3942d548f3daec02a833983ed9b2b69f75e2cd1b74a8ce1b874f3fd33fde55L153-R177)
[[5]](diffhunk://#diff-0c0a89e812ff4625d165417da14f1c3f203e5ac7907555ae4fde122f3dddcf7aL115-L130)
[[6]](diffhunk://#diff-34581ec47c37b0d2e1d9b59696225c47342930694e732db06cbdf653ceb2c2d7L205-R234)
[[7]](diffhunk://#diff-34581ec47c37b0d2e1d9b59696225c47342930694e732db06cbdf653ceb2c2d7R334).

These changes improve maintainability and reduce the risk of
inconsistent registration behavior across modules.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-27 19:16:55 +08:00
Kai Tao
7455d63bb5 Settings: Settings search fixes (#41381)
<!-- 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
Fix 3 issues

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

- [x] Closes: #41369, #41374, #41380
- [ ] **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/0e0df9fb-5aca-4b26-9d53-e6ddc49cab04

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2025-08-27 14:51:36 +08:00
Niels Laute
bb6b36af3f Search UX improvements (#41386)
### Fixing visual glitch in the 'Show all results' template:

Before
<img width="588" height="103" alt="image"
src="https://github.com/user-attachments/assets/00da60ea-d0ab-4ffe-8f69-75be7e537d63"
/>

After
<img width="598" height="70" alt="image"
src="https://github.com/user-attachments/assets/dc859731-8783-494a-b561-ea6396ca69b3"
/>


### Displaying "Show results for * search term *" on search results page
Before
<img width="716" height="239" alt="image"
src="https://github.com/user-attachments/assets/a80e6e58-df88-47b2-85ab-c39cbdc88690"
/>

After
<img width="349" height="264" alt="image"
src="https://github.com/user-attachments/assets/99029ee6-94f7-4454-a443-640c22f9e6f3"
/>


### Using Accent Color for the search highlight for better visibility
and fixing a bug
Before

![highlight-bug](https://github.com/user-attachments/assets/2d4c0f5b-4030-4c10-a4ec-c7c5023034f0)


After


![fix-accent](https://github.com/user-attachments/assets/62a223a9-9360-4297-9127-5aaef82c162f)

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2025-08-27 13:42:30 +08:00
Jiří Polášek
3ebf0f741a Settings UI: Fix spelling error and make spell checker happy (#41403)
## Summary of the Pull Request

- Renames `NavigatablePage` to `NavigablePage` to fix a spelling error.
- Reverts related entries in `exclude.txt` that triggered a forbidden
pattern error in the spell checker.
- The spell checker does not allow PascalCase words like
`NavigatablePage` in `exclude.txt` because of its automatic casing
rules.

 
Regression: #41285

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2025-08-27 11:29:14 +08:00
Shawn Yuan
e8e1431e15 Fix localization issue for shortcut conflict window (#41378)
<!-- 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 localization for conflict message shown in shortcut conflict
window and shortcut dialog.

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

- [x] Closes: #41373 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **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

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-08-27 09:55:43 +08:00
Niels Laute
2d35fd8530 [Fix] Adding Toolkit's TitleBar manually (#41383)
Temp fix for a VS bug when building the `PowerToys.Settings` project. 

There seems to be something wrong with the Labs TitleBar package, so
following up with the team to get it resolved. Meanwhile, I've moved the
Toolkit's source code for TitleBar to Settings as a custom control.

Once the package is fixed we can revert this change.
2025-08-27 09:27:11 +08:00
Michael Jolley
13d950a40a Adding sort to DateTime extension results (#41389)
Closes #41385

Adds a sort to the DateTime extension results.

<img width="1001" height="602" alt="image"
src="https://github.com/user-attachments/assets/ccccae6a-c4a4-460f-a6d3-3325ecfc53da"
/>

<img width="992" height="606" alt="image"
src="https://github.com/user-attachments/assets/b8af51bd-cbd0-4341-ac46-0fb3e97ec2ac"
/>
2025-08-26 15:34:06 -05:00
Jiří Polášek
3eb8f96f3e CmdPal: Fix resource key name NavigationPaneClosed to match literal in code and prevent crash (#41361)
## Summary of the Pull Request

Corrects a mismatch between the resource key and the actual literal used
in code. The key was incorrectly named `NavigationPageOpened`; it now
correctly uses `NavigationPaneClosed`. Introduced in #41016.


## PR Checklist

- [x] Closes: #41362
- [ ] **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
2025-08-26 14:30:13 -05:00
Mike Griese
11218ea4d8 CmdPal: Make debugging extension load errors easier (#41251)
Turns out we didn't log all the HRESULTs for failing to load an
extension. This adds more logging.

It also adds a context command on the log command to make it easier to
get to the log files
2025-08-26 10:54:58 -05:00
203 changed files with 6879 additions and 1337 deletions

View File

@@ -49,6 +49,7 @@ ALPHATYPE
AModifier
amr
ANDSCANS
animatedvisuals
Animnate
ANull
AOC
@@ -69,6 +70,7 @@ APPMODEL
APPNAME
appref
appsettings
appsfeatures
appwindow
appwiz
appxpackage
@@ -304,6 +306,7 @@ CXVIRTUALSCREEN
CYSCREEN
CYSMICON
CYVIRTUALSCREEN
Czechia
cziplib
Dac
dacl
@@ -328,6 +331,7 @@ Deact
debugbreak
decryptor
Dedup
Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR
@@ -433,6 +437,7 @@ EDITSHORTCUTS
EDITTEXT
EFile
ekus
emojis
ENABLEDELAYEDEXPANSION
ENABLEDPOPUP
ENABLETAB
@@ -797,6 +802,7 @@ KEYBOARDMANAGEREDITORLIBRARYWRAPPER
keyboardmanagerstate
keyboardmanagerui
keyboardtester
keycap
KEYEVENTF
KEYIMAGE
keynum
@@ -1024,8 +1030,6 @@ MYICON
NAMECHANGE
namespaceanddescendants
nao
Navigatable
NavigatablePage
NCACTIVATE
ncc
NCCALCSIZE
@@ -1316,6 +1320,7 @@ PRODUCTVERSION
Progman
programdata
projectname
projitems
PROPERTYKEY
Propset
PROPVARIANT
@@ -1395,6 +1400,7 @@ regkey
regroot
regsvr
REINSTALLMODE
releaseblog
reloadable
Relogger
remappings
@@ -1778,10 +1784,13 @@ UACUI
UAL
uap
UBR
UBreak
ubrk
UCallback
ucrt
ucrtd
uefi
UError
uesc
UFlags
UHash
@@ -1851,6 +1860,7 @@ VFT
vget
vgetq
viewmodels
virama
VIRTKEY
VIRTUALDESK
VISEGRADRELAY

View File

@@ -260,3 +260,7 @@ Process Process
# ZoomIt menu items with accelerator keys
E&xit
St&yle
# This matches a relative clause where the relative pronoun "that" is omitted.
# Example: "Gets or sets the window the TitleBar should configure."
\bthe\s+\w+\s+the\b

View File

@@ -0,0 +1,38 @@
name: Manual Batch Issue Deduplication
on:
workflow_dispatch:
inputs:
issue_numbers:
description: "JSON array of issue numbers to deduplicate (e.g. [101,102,103])"
required: true
since:
description: "Only compare against issues created after this date (ISO 8601, e.g. 2019-05-05T00:00:00Z)"
required: false
default: "2019-05-05T00:00:00Z"
label_as_duplicate:
description: "Apply duplicate label if duplicates are found (true/false)"
required: false
default: "true"
permissions:
models: read
issues: write
jobs:
deduplicate:
runs-on: ubuntu-latest
strategy:
matrix:
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run GenAI Issue Deduplicator
uses: pelikhan/action-genai-issue-dedup@v0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_issue: ${{ matrix.issue }}
label_as_duplicate: ${{ github.event.inputs.label_as_duplicate }}

View File

@@ -23,7 +23,6 @@
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250703-build.2173" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.TitleBar" Version="0.0.1-build.2206" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

@@ -1499,7 +1499,6 @@ SOFTWARE.
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
- CommunityToolkit.Labs.WinUI.TitleBar
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections

View File

@@ -262,7 +262,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\EventLocker.h = src\common\utils\EventLocker.h
src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h
src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h
src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
src\common\utils\exec.h = src\common\utils\exec.h
src\common\utils\game_mode.h = src\common\utils\game_mode.h
src\common\utils\gpo.h = src\common\utils\gpo.h
@@ -282,6 +281,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\registry.h = src\common\utils\registry.h
src\common\utils\resources.h = src\common\utils\resources.h
src\common\utils\serialized.h = src\common\utils\serialized.h
src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
src\common\utils\string_utils.h = src\common\utils\string_utils.h
src\common\utils\timeutil.h = src\common\utils\timeutil.h
src\common\utils\UnhandledExceptionHandler.h = src\common\utils\UnhandledExceptionHandler.h
@@ -801,6 +801,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DwellCursor", "src\modules\MouseUtils\DwellCursor\DwellCursor.vcxproj", "{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2695,22 +2697,6 @@ Global
{61CBF221-9452-4934-B685-146285E080D7}.Release|ARM64.Build.0 = Release|ARM64
{61CBF221-9452-4934-B685-146285E080D7}.Release|x64.ActiveCfg = Release|x64
{61CBF221-9452-4934-B685-146285E080D7}.Release|x64.Build.0 = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.ActiveCfg = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.Build.0 = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.Build.0 = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.ActiveCfg = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.Build.0 = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.ActiveCfg = Debug|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.Build.0 = Debug|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.ActiveCfg = Debug|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.Build.0 = Debug|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.ActiveCfg = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.ActiveCfg = Debug|ARM64
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.Build.0 = Debug|ARM64
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.ActiveCfg = Debug|x64
@@ -2727,14 +2713,22 @@ Global
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.Build.0 = Release|ARM64
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.ActiveCfg = Release|x64
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.Build.0 = Release|x64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.Build.0 = Debug|x64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.ActiveCfg = Release|ARM64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.Build.0 = Release|ARM64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.ActiveCfg = Release|x64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.Build.0 = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.ActiveCfg = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.Build.0 = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.Build.0 = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.ActiveCfg = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.Build.0 = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.ActiveCfg = Debug|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.Build.0 = Debug|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.ActiveCfg = Debug|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.Build.0 = Debug|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.ActiveCfg = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.Build.0 = Debug|ARM64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|x64.ActiveCfg = Debug|x64
@@ -2919,6 +2913,14 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Debug|ARM64.ActiveCfg = Debug|ARM64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Debug|ARM64.Build.0 = Debug|ARM64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Debug|x64.ActiveCfg = Debug|x64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Debug|x64.Build.0 = Debug|x64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Release|ARM64.ActiveCfg = Release|ARM64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Release|ARM64.Build.0 = Release|ARM64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Release|x64.ActiveCfg = Release|x64
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3194,10 +3196,10 @@ Global
{9BC1C986-1E97-4D07-A7B1-CE226C239EFA} = {2F305555-C296-497E-AC20-5FA1B237996A}
{99CA1509-FB73-456E-AFAF-AB89C017BD72} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}
{61CBF221-9452-4934-B685-146285E080D7} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2}
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {68328142-5B31-4715-BCBB-7B6345EE0971}
{38F187B2-6638-5A40-072F-DBE5E54070A0} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE} = {C3081D9A-1586-441A-B5F4-ED815B3719C1}
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2}
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {68328142-5B31-4715-BCBB-7B6345EE0971}
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{14AFD976-B4D2-49D0-9E6C-AA93CC061B8A} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{9D3F3793-EFE3-4525-8782-238015DABA62} = {66E1534A-1587-42B2-912F-45C994D32904}
@@ -3237,6 +3239,7 @@ Global
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61} = {322566EF-20DC-43A6-B9F8-616AF942579A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

199
README.md
View File

@@ -35,19 +35,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- 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.94%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
This is our preferred method.
@@ -93,118 +93,145 @@ For guidance on developing for PowerToys, please read the [developer docs](./doc
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.93 - Aug 2025 Update
### 0.94 - Sep 2025 Update
In this release, we focused on new features, stability, optimization improvements, and automation.
For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
**✨Highlights**
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience.
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run.
- Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK.
- Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)!
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations.
- Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage.
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for singlebutton cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
- Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
### Always On Top
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
### Command Palette
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
- Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved UI design with better text sizing and alignment.
- Fixed keyboard shortcuts to work better in text boxes and context menus.
- Added right-click context menus with critical command styling and separators.
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality.
- Fixed context menu crashes with better type handling.
- Fixed "Reload" command to work with both uppercase and lowercase letters.
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window focus not returning to previous app properly.
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)!
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Standardized null checks using C# pattern matching for safer behavior.
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
- Added single-select filters to DynamicListPage and updated Windows Services sample.
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Propagated alias changes safely and resolved conflicts across view models.
- Allowed providers to override Dispose with a virtual method.
- Fixed memory leaks by cleaning up removed or cancelled list items.
- Sorted DateTime extension results by relevance for better usability.
- Reduced search text “jiggling” by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- Added “evil” sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
### Command Palette extensions
- Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature.
- Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs).
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)!
- Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
- Added keyboard shortcuts to the *Apps* context menu items for faster access.
- Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality.
- Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer.
- Added command history to the *Run* page for easier access to previous commands.
- Fixed directory path handling in *Run* fallback for better file navigation.
- Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added fallback command to *Windows Settings* extension for better search results.
- Re-enabled *Clipboard History* feature with proper window handling.
- Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input.
- Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows.
- Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)!
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added app icons to the All Apps "Run" context command when available.
- Restored missing builtin icons by standardizing extension dependencies.
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
### Hosts File Editor
- Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
### Image Resizer
- Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
### Mouse Utilities
- Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen.
- Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
### Mouse Without Borders
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
### Peek
- Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)!
- Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
### PowerRename
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
### Quick Accent
- Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
- Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
### Settings
- Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list.
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand.
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing.
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings.
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window.
- Resolved a settings crash related to incompatible property names in ZoomIt configuration.
- Added telemetry to track usage of the new shortcut conflict detection workflow.
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
- Introduced Settings search with fuzzy matching and navigation to specific controls.
- Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
- Localized conflict messages in the conflict window and dialog.
### Installer
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
### Documentation
- Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)!
- Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)!
- Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)!
- Adds docs for building the installer locally and testing winget installs.
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
### Development
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)!
- Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)!
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother.
- Replaced NuGet feed with Azure Artifacts for better package management.
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
- Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts.
- Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck.
- Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)!
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality.
- Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches.
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Added accessibility IDs to CmdPal UI for stable UI tests.
- Rewrote system command tests with a new test base and cleaner patterns.
- Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- Cleans up AIgenerated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
### What is being planned over the next few releases
For [v0.94][github-next-release-work], we'll work on the items below:
For [v0.95][github-next-release-work], we'll work on the items below:
- Continued Command Palette polish
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
- Working on upgrading the installer to WiX 5
- Working on shortcut conflict detection
- Working on setting search
- Upgrading Keyboard Manager's editor UI
- UI tweaking utility with day/night theme switcher
- DSC v3 support for top utilities
- New UI automation tests
- Stability, bug fixes

View File

@@ -76,6 +76,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
1. Windows 10 April 2018 Update (version 1803) or newer
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer
1. A local clone of the PowerToys repository
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
### Install Visual Studio dependencies

View File

@@ -32,17 +32,8 @@ namespace ManagedCommon
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
{
string basePath;
if (isLocalLow)
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
string basePath = Path.GetDirectoryName(versionedPath);
if (!Directory.Exists(versionedPath))
{
@@ -59,6 +50,22 @@ namespace ManagedCommon
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
}
public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false)
{
string basePath;
if (isLocalLow)
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
return versionedPath;
}
/// <summary>
/// Deletes old version log folders, keeping only the current version's folder.
/// </summary>
@@ -115,13 +122,13 @@ namespace ManagedCommon
{
var exMessage =
message + Environment.NewLine +
ex.GetType() + ": " + ex.Message + Environment.NewLine;
ex.GetType() + " (" + ex.HResult + "): " + ex.Message + Environment.NewLine;
if (ex.InnerException != null)
{
exMessage +=
"Inner exception: " + Environment.NewLine +
ex.InnerException.GetType() + ": " + ex.InnerException.Message + Environment.NewLine;
ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
}
exMessage +=

View File

@@ -20,27 +20,14 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
x:Name="titleBar"
Height="32"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/EnvironmentVariables/EnvironmentVariables.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/EnvironmentVariables/EnvironmentVariables.ico" />
</TitleBar.LeftHeader>
</TitleBar>
</Grid>
</winuiex:WindowEx>

View File

@@ -4,22 +4,19 @@
using System;
using System.Runtime.InteropServices;
using EnvironmentVariables.Win32;
using EnvironmentVariablesUILib;
using EnvironmentVariablesUILib.Helpers;
using EnvironmentVariablesUILib.ViewModels;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
namespace EnvironmentVariables
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
private EnvironmentVariablesMainPage MainPage { get; }
@@ -34,8 +31,9 @@ namespace EnvironmentVariables
AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
var loader = ResourceLoaderInstance.ResourceLoader;
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
Title = title;
AppTitleTextBlock.Text = title;
titleBar.Title = title;
var handle = this.GetWindowHandle();
RegisterWindow(handle);

View File

@@ -19,6 +19,26 @@
class FileLocksmithModule : public PowertoyModuleIface
{
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
Logger::info(L"File Locksmith context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered");
#endif
}
}
public:
FileLocksmithModule()
{
@@ -88,21 +108,16 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
#endif
m_enabled = true;
UpdateRegistration(m_enabled);
}
virtual void disable() override
{
Logger::info(L"File Locksmith disabled");
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered (Win10)");
#endif
m_enabled = false;
UpdateRegistration(m_enabled);
}
virtual bool is_enabled() override
@@ -135,6 +150,7 @@ private:
{
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
UpdateRegistration(m_enabled);
Trace::EnableFileLocksmith(m_enabled);
}

View File

@@ -20,30 +20,15 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
x:Name="AppTitleBar"
Height="32"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightDragColumn" Width="*" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/FileLocksmith/Icon.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/FileLocksmith/Icon.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<views:MainPage x:Name="mainPage" Grid.Row="1" />
</Grid>
</winuiex:WindowEx>
</winuiex:WindowEx>

View File

@@ -18,30 +18,16 @@ namespace FileLocksmithUI
{
InitializeComponent();
mainPage.ViewModel.IsElevated = isElevated;
SetTitleBar(titleBar);
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
Activated += MainWindow_Activated;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico");
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
var loader = ResourceLoaderInstance.ResourceLoader;
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
Title = title;
AppTitleTextBlock.Text = title;
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
AppTitleTextBlock.Foreground =
(SolidColorBrush)App.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
AppTitleTextBlock.Foreground =
(SolidColorBrush)App.Current.Resources["WindowCaptionForeground"];
}
titleBar.Title = title;
}
public void Dispose()

View File

@@ -190,7 +190,7 @@
TextWrapping="Wrap" />
</ContentDialog>
<ContentDialog x:Name="ProcessFilesListDialog" x:Uid="ProcessFilesListDialog">
<ScrollViewer Padding="15" HorizontalScrollBarVisibility="Auto">
<ScrollViewer Padding="16" HorizontalScrollBarVisibility="Auto">
<TextBlock
x:Name="ProcessFilesListDialogTextBlock"
x:Uid="ProcessFilesListDialogTextBlock"

View File

@@ -20,27 +20,14 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
x:Name="titleBar"
Height="32"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/Hosts/Hosts.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/Hosts/Hosts.ico" />
</TitleBar.LeftHeader>
</TitleBar>
</Grid>
</winuiex:WindowEx>

View File

@@ -9,19 +9,15 @@ using HostsUILib.Helpers;
using HostsUILib.Views;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.ApplicationModel.Resources;
using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Hosts
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
private HostsMainPage MainPage { get; }
@@ -38,31 +34,18 @@ namespace Hosts
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
Title = title;
AppTitleTextBlock.Text = title;
titleBar.Title = title;
var handle = this.GetWindowHandle();
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(handle);
WindowHelpers.BringToForeground(handle);
Activated += MainWindow_Activated;
MainPage = Host.GetService<HostsMainPage>();
PowerToysTelemetry.Log.WriteEvent(new HostEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
AppTitleTextBlock.Foreground = (SolidColorBrush)App.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
AppTitleTextBlock.Foreground = (SolidColorBrush)App.Current.Resources["WindowCaptionForeground"];
}
}
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
MainGrid.Children.Add(MainPage);

View File

@@ -31,7 +31,11 @@ struct CommonState
Measurement::Unit units = Measurement::Unit::Pixel;
POINT cursorPosSystemSpace = {}; // updated atomically
#pragma warning(push)
#pragma warning(disable : 4324)
alignas(8) POINT cursorPosSystemSpace = {}; // updated atomically
#pragma warning(pop)
std::atomic_bool closeOnOtherMonitors = false;
float GetPhysicalPx2MmRatio(HWND window) const

View File

@@ -0,0 +1,7 @@
#include <windows.h>
#include "resource.h"
STRINGTABLE
BEGIN
IDS_MODULE_NAME "DwellCursor"
END

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{6C2AD2F7-8BDF-4C5E-B3F5-7E1B7A0A8A61}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>DwellCursor</RootNamespace>
<ProjectName>DwellCursor</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
<TargetName>PowerToys.DwellCursor</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="DwellIndicator.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="DwellIndicator.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="DwellCursor.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<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.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,830 @@
#include "pch.h"
#include "DwellIndicator.h"
#include <gdiplus.h>
#include <cmath>
#pragma comment(lib, "gdiplus.lib")
#pragma comment(lib, "dwmapi.lib")
using namespace Gdiplus;
/**
* @brief Implementation class for the dwell indicator using the Pimpl idiom
*
* This class handles all the visual indicator functionality:
* - Creates a transparent, topmost window at cursor position
* - Draws a circular progress arc using GDI+
* - Updates progress smoothly during countdown
* - Uses system accent color for theming
*/
class DwellIndicatorImpl
{
public:
DwellIndicatorImpl() = default;
~DwellIndicatorImpl() = default;
// Public interface methods
bool Initialize();
void Show(int x, int y);
void UpdateProgress(float progress);
void Hide();
void Cleanup();
private:
// Window management
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept;
bool CreateIndicatorWindow();
void DrawIndicator(HDC hdc);
float GetDpiScale() const;
// Window class and visual constants
static constexpr auto m_className = L"DwellCursorIndicator";
static constexpr auto m_windowTitle = L"PowerToys Dwell Cursor Indicator";
static constexpr float kIndicatorRadius = 20.0f; // Circle radius in pixels
static constexpr float kStrokeWidth = 3.0f; // Arc stroke width in pixels
// Window and positioning state
HWND m_hwnd = NULL; // Handle to the indicator window
HINSTANCE m_hinstance = NULL; // Module instance handle
bool m_isVisible = false; // Current visibility state
int m_currentX = 0; // Last shown X position
int m_currentY = 0; // Last shown Y position
float m_progress = 0.0f; // Current progress (0.0 to 1.0)
// GDI+ resources
ULONG_PTR m_gdiplusToken = 0; // GDI+ initialization token
friend class DwellIndicator;
};
/**
* @brief Window procedure for the indicator window
*
* Handles window messages for the transparent indicator overlay:
* - WM_PAINT: Triggers redraw of the progress arc
* - WM_NCHITTEST: Returns HTTRANSPARENT to allow mouse events to pass through
* - WM_DESTROY: Standard cleanup
*
* @param hWnd Window handle
* @param message Windows message ID
* @param wParam Message parameter
* @param lParam Message parameter
* @return Message handling result
*/
LRESULT CALLBACK DwellIndicatorImpl::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept
{
DwellIndicatorImpl* pThis = nullptr;
// Retrieve the instance pointer stored during window creation
if (message == WM_NCCREATE)
{
// During window creation, extract the 'this' pointer from creation params
CREATESTRUCT* pcs = reinterpret_cast<CREATESTRUCT*>(lParam);
pThis = static_cast<DwellIndicatorImpl*>(pcs->lpCreateParams);
SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pThis));
}
else
{
// For all other messages, retrieve the stored 'this' pointer
pThis = reinterpret_cast<DwellIndicatorImpl*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
}
switch (message)
{
case WM_PAINT:
// Redraw the indicator - this is where our visual progress arc gets drawn
if (pThis)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
pThis->DrawIndicator(hdc); // Draw the circular progress indicator
EndPaint(hWnd, &ps);
}
return 0;
case WM_NCHITTEST:
// Restore transparent mouse behavior - allow clicks to pass through
return HTTRANSPARENT;
case WM_DESTROY:
// DO NOT call PostQuitMessage(0) for overlay windows!
// This was interfering with PowerToys main message loop and causing settings menu issues
// Just let the window be destroyed normally
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
/**
* @brief Initialize the indicator system
*
* Sets up GDI+ graphics system and creates the indicator window.
* This must be called before any Show/Update operations.
*
* @return true if initialization successful, false on failure
*/
bool DwellIndicatorImpl::Initialize()
{
m_hinstance = GetModuleHandle(NULL);
// Initialize GDI+ graphics system for smooth drawing
GdiplusStartupInput gdiplusStartupInput;
Gdiplus::Status status = GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
if (status != Gdiplus::Ok)
{
// GDI+ initialization failed - no visual indicator will work
return false;
}
// Create the transparent overlay window
bool windowCreated = CreateIndicatorWindow();
if (!windowCreated)
{
// Clean up GDI+ if window creation failed
if (m_gdiplusToken != 0)
{
GdiplusShutdown(m_gdiplusToken);
m_gdiplusToken = 0;
}
}
return windowCreated;
}
/**
* @brief Create the transparent indicator window
*
* Creates a layered, transparent, topmost window that:
* - Appears above all other windows
* - Allows mouse events to pass through
* - Has no border, title bar, or decorations
* - Is positioned and sized later when shown
*
* @return true if window created successfully, false on failure
*/
bool DwellIndicatorImpl::CreateIndicatorWindow()
{
WNDCLASS wc{};
// Set DPI awareness for proper scaling on high-DPI displays
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
// Register window class only if not already registered
if (!GetClassInfoW(m_hinstance, m_className, &wc))
{
wc.lpfnWndProc = WndProc; // Our window procedure
wc.hInstance = m_hinstance; // Module instance
wc.hIcon = LoadIcon(m_hinstance, IDI_APPLICATION); // Default icon
wc.hCursor = LoadCursor(nullptr, IDC_ARROW); // Default cursor
wc.hbrBackground = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH)); // Transparent background
wc.lpszClassName = m_className; // Class name for window
if (!RegisterClassW(&wc))
{
// Failed to register window class
DWORD error = GetLastError();
// Note: Can't use Logger here as it might not be available in all contexts
return false;
}
}
// Create window with transparency and mouse pass-through restored
DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
m_hwnd = CreateWindowExW(
exStyle, // Extended window styles with transparency restored
m_className, // Window class name
m_windowTitle, // Window title (not visible)
WS_POPUP, // Window style - popup with no decorations
0, 0, 100, 100, // Initial position and size (will be adjusted in Show())
nullptr, // No parent window
nullptr, // No menu
m_hinstance, // Module instance
this); // Pass 'this' pointer for WndProc to access
if (!m_hwnd)
{
DWORD error = GetLastError();
// Note: Can't use Logger here as it might not be available in all contexts
}
else
{
OutputDebugStringA("DwellIndicator: Created transparent layered window with mouse pass-through\n");
}
return m_hwnd != nullptr;
}
/**
* @brief Show the indicator at specified cursor position
*
* Positions the window centered on the cursor location and makes it visible.
* The window size is calculated based on indicator radius and DPI scaling.
*
* @param x Cursor X coordinate in screen pixels
* @param y Cursor Y coordinate in screen pixels
*/
void DwellIndicatorImpl::Show(int x, int y)
{
// Check if window handle is valid before proceeding
if (!m_hwnd)
{
OutputDebugStringA("DwellIndicator: ERROR - Window handle is NULL, cannot show indicator\n");
return;
}
// **CRITICAL FIX: Reset progress state immediately when showing at new position**
float oldProgress = m_progress;
m_progress = 0.0f;
// Store current position for reference
m_currentX = x;
m_currentY = y;
// Calculate window size based on indicator radius and DPI scaling
const float dpiScale = GetDpiScale();
const int windowSize = static_cast<int>((kIndicatorRadius * 2 + kStrokeWidth * 2 + 10) * dpiScale);
// Calculate final window position (centered on cursor)
const int windowX = x - windowSize / 2;
const int windowY = y - windowSize / 2;
// Log detailed positioning information for debugging
char debugMsg[512];
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: SHOW - Cursor:(%d,%d) Window:(%d,%d) Size:%dx%d DPI:%.2f Progress: %.3f->0.0\n",
x, y, windowX, windowY, windowSize, windowSize, dpiScale, oldProgress);
OutputDebugStringA(debugMsg);
// **GDI+ EXPERT FIX: Use UpdateLayeredWindow for proper transparency reset**
// This is the correct way to handle layered windows with transparency
// First hide the window to ensure clean state
if (m_isVisible)
{
ShowWindow(m_hwnd, SW_HIDE);
m_isVisible = false;
}
// Position window (while hidden for clean transition)
BOOL setWindowPosResult = SetWindowPos(m_hwnd, HWND_TOPMOST,
windowX, windowY, // Calculated position
windowSize, windowSize, // Square window to contain circle
SWP_NOACTIVATE | SWP_HIDEWINDOW); // Position but keep hidden for now
if (!setWindowPosResult)
{
DWORD error = GetLastError();
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: ERROR - SetWindowPos failed with error %lu\n", error);
OutputDebugStringA(debugMsg);
}
// **GDI+ EXPERT FIX: Create clean bitmap and use UpdateLayeredWindow**
// This completely clears any previous drawing artifacts
HDC screenDC = GetDC(NULL);
HDC memoryDC = CreateCompatibleDC(screenDC);
// Create 32-bit bitmap with alpha channel for proper transparency
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = windowSize;
bmi.bmiHeader.biHeight = -windowSize; // Top-down DIB
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32; // 32-bit with alpha
bmi.bmiHeader.biCompression = BI_RGB;
void* pvBits = nullptr;
HBITMAP hBitmap = CreateDIBSection(screenDC, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0);
if (hBitmap && memoryDC)
{
HBITMAP oldBitmap = static_cast<HBITMAP>(SelectObject(memoryDC, hBitmap));
// **CRITICAL: Clear the entire bitmap with transparent pixels**
// This ensures no artifacts from previous drawings
// Fix for C26451: Use safe arithmetic to prevent overflow
const size_t bitmapSizeBytes = static_cast<size_t>(windowSize) * static_cast<size_t>(windowSize) * 4ULL;
memset(pvBits, 0, bitmapSizeBytes); // Clear to transparent
// Create GDI+ Graphics object from memory DC
Graphics graphics(memoryDC);
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetCompositingMode(CompositingModeSourceOver);
graphics.SetCompositingQuality(CompositingQualityHighQuality);
// **GDI+ EXPERT: Use Graphics::Clear with transparent color**
// This properly clears the alpha channel
graphics.Clear(Color(0, 0, 0, 0)); // Fully transparent
// Draw only the background circle (no progress arc yet since progress = 0.0)
const float centerX = windowSize / 2.0f;
const float centerY = windowSize / 2.0f;
const float radius = kIndicatorRadius * dpiScale;
const float strokeWidth = kStrokeWidth * dpiScale;
// Get system accent color
DWORD accentColor = 0;
BOOL isOpaque = FALSE;
Color backgroundCircleColor;
if (SUCCEEDED(DwmGetColorizationColor(&accentColor, &isOpaque)))
{
const BYTE r = (accentColor >> 16) & 0xFF;
const BYTE g = (accentColor >> 8) & 0xFF;
const BYTE b = accentColor & 0xFF;
const BYTE bgR = static_cast<BYTE>((r + 128) / 2);
const BYTE bgG = static_cast<BYTE>((g + 128) / 2);
const BYTE bgB = static_cast<BYTE>((b + 128) / 2);
backgroundCircleColor = Color(80, bgR, bgG, bgB);
}
else
{
backgroundCircleColor = Color(80, 160, 160, 160);
}
// Draw background circle
RectF ellipseRect(centerX - radius, centerY - radius, radius * 2, radius * 2);
Pen bgPen(backgroundCircleColor, strokeWidth * 0.6f);
graphics.DrawEllipse(&bgPen, ellipseRect);
// **GDI+ EXPERT: Use UpdateLayeredWindow for artifact-free display**
POINT ptSrc = {0, 0};
POINT ptDst = {windowX, windowY};
SIZE size = {windowSize, windowSize};
BLENDFUNCTION blend = {};
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA; // Use per-pixel alpha
BOOL updateResult = UpdateLayeredWindow(m_hwnd, screenDC, &ptDst, &size,
memoryDC, &ptSrc, 0, &blend, ULW_ALPHA);
// Cleanup
SelectObject(memoryDC, oldBitmap);
DeleteObject(hBitmap);
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: UpdateLayeredWindow result: %s\n",
updateResult ? "SUCCESS" : "FAILED");
OutputDebugStringA(debugMsg);
}
DeleteDC(memoryDC);
ReleaseDC(NULL, screenDC);
// Now show the window with clean, artifact-free display
ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
m_isVisible = true;
OutputDebugStringA("DwellIndicator: SHOW Complete - Clean display with no artifacts\n");
}
/**
* @brief Update the progress of the countdown indicator
*
* Updates the internal progress value and triggers a redraw.
* Progress is clamped to [0.0, 1.0] range.
*
* @param progress Progress value from 0.0 (start) to 1.0 (complete)
*/
void DwellIndicatorImpl::UpdateProgress(float progress)
{
// Clamp progress to valid range [0.0, 1.0]
if (progress < 0.0f) progress = 0.0f;
if (progress > 1.0f) progress = 1.0f;
// Log progress updates for debugging
char debugMsg[256];
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: UPDATE Progress %.3f -> %.3f - Window: %s, Visible: %s\n",
m_progress, progress,
m_hwnd ? "VALID" : "NULL",
m_isVisible ? "TRUE" : "FALSE");
OutputDebugStringA(debugMsg);
float oldProgress = m_progress;
m_progress = progress;
// **GDI+ EXPERT FIX: Use UpdateLayeredWindow for artifact-free updates**
if (m_hwnd && m_isVisible)
{
// Get window dimensions
RECT rect;
GetClientRect(m_hwnd, &rect);
int windowSize = rect.right - rect.left;
// Create memory DC and bitmap for off-screen rendering
HDC screenDC = GetDC(NULL);
HDC memoryDC = CreateCompatibleDC(screenDC);
// Create 32-bit bitmap with alpha channel
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = windowSize;
bmi.bmiHeader.biHeight = -windowSize; // Top-down DIB
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32; // 32-bit with alpha
bmi.bmiHeader.biCompression = BI_RGB;
void* pvBits = nullptr;
HBITMAP hBitmap = CreateDIBSection(screenDC, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0);
if (hBitmap && memoryDC)
{
HBITMAP oldBitmap = static_cast<HBITMAP>(SelectObject(memoryDC, hBitmap));
// **CRITICAL: Clear entire bitmap to transparent**
const size_t bitmapSizeBytes = static_cast<size_t>(windowSize) * static_cast<size_t>(windowSize) * 4ULL;
memset(pvBits, 0, bitmapSizeBytes);
// Create GDI+ Graphics object
Graphics graphics(memoryDC);
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetCompositingMode(CompositingModeSourceOver);
graphics.SetCompositingQuality(CompositingQualityHighQuality);
// **GDI+ EXPERT: Proper alpha channel clearing**
graphics.Clear(Color(0, 0, 0, 0));
// Calculate drawing parameters
const float dpiScale = GetDpiScale();
const float centerX = windowSize / 2.0f;
const float centerY = windowSize / 2.0f;
const float radius = kIndicatorRadius * dpiScale;
const float strokeWidth = kStrokeWidth * dpiScale;
// Get system colors
DWORD accentColor = 0;
BOOL isOpaque = FALSE;
Color progressColor;
Color backgroundCircleColor;
if (SUCCEEDED(DwmGetColorizationColor(&accentColor, &isOpaque)))
{
const BYTE a = 255;
const BYTE r = (accentColor >> 16) & 0xFF;
const BYTE g = (accentColor >> 8) & 0xFF;
const BYTE b = accentColor & 0xFF;
progressColor = Color(a, r, g, b);
const BYTE bgR = static_cast<BYTE>((r + 128) / 2);
const BYTE bgG = static_cast<BYTE>((g + 128) / 2);
const BYTE bgB = static_cast<BYTE>((b + 128) / 2);
backgroundCircleColor = Color(80, bgR, bgG, bgB);
}
else
{
progressColor = Color(255, 0, 120, 215);
backgroundCircleColor = Color(80, 160, 160, 160);
}
// Create bounding rectangle
RectF ellipseRect(centerX - radius, centerY - radius, radius * 2, radius * 2);
// Draw background circle
Pen bgPen(backgroundCircleColor, strokeWidth * 0.6f);
graphics.DrawEllipse(&bgPen, ellipseRect);
// Draw progress arc if we have progress
if (m_progress > 0.0f)
{
Pen progressPen(progressColor, strokeWidth);
progressPen.SetStartCap(LineCapRound);
progressPen.SetEndCap(LineCapRound);
const float startAngle = -90.0f; // 12 o'clock
const float sweepAngle = m_progress * 360.0f;
graphics.DrawArc(&progressPen, ellipseRect, startAngle, sweepAngle);
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: Drew arc - Progress %.3f, SweepAngle %.1f degrees\n",
m_progress, sweepAngle);
OutputDebugStringA(debugMsg);
}
// **GDI+ EXPERT: Update layered window with new content**
POINT ptSrc = {0, 0};
POINT ptDst = {m_currentX - windowSize/2, m_currentY - windowSize/2};
SIZE size = {windowSize, windowSize};
BLENDFUNCTION blend = {};
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;
BOOL updateResult = UpdateLayeredWindow(m_hwnd, screenDC, &ptDst, &size,
memoryDC, &ptSrc, 0, &blend, ULW_ALPHA);
// Cleanup
SelectObject(memoryDC, oldBitmap);
DeleteObject(hBitmap);
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: UPDATE Complete - UpdateLayeredWindow: %s (%.3f->%.3f)\n",
updateResult ? "SUCCESS" : "FAILED", oldProgress, progress);
OutputDebugStringA(debugMsg);
}
DeleteDC(memoryDC);
ReleaseDC(NULL, screenDC);
}
else
{
OutputDebugStringA("DwellIndicator: UPDATE Skipped - window not ready or not visible\n");
}
}
/**
* @brief Draw the circular progress indicator
*
* **NOTE: This method is now primarily for fallback WM_PAINT handling**
* The main rendering is done through UpdateLayeredWindow in Show() and UpdateProgress()
* for artifact-free display on layered windows.
*
* @param hdc Device context to draw into
*/
void DwellIndicatorImpl::DrawIndicator(HDC hdc)
{
// Log drawing calls for debugging
char debugMsg[256];
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: DRAW (Fallback WM_PAINT) - Progress %.3f, Visible: %s\n",
m_progress, m_isVisible ? "TRUE" : "FALSE");
OutputDebugStringA(debugMsg);
// **GDI+ EXPERT: For WM_PAINT on layered windows, we need special handling**
// Generally, UpdateLayeredWindow bypasses WM_PAINT, but this provides fallback
// Set up GDI+ graphics object with optimal settings for layered windows
Graphics graphics(hdc);
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetCompositingMode(CompositingModeSourceOver);
graphics.SetCompositingQuality(CompositingQualityHighQuality);
graphics.SetPixelOffsetMode(PixelOffsetModeHighQuality);
// Get window client area dimensions
RECT rect;
GetClientRect(m_hwnd, &rect);
const float centerX = (rect.right - rect.left) / 2.0f;
const float centerY = (rect.bottom - rect.top) / 2.0f;
// **GDI+ EXPERT: Proper clearing for layered windows**
// Use Graphics::Clear instead of FillRectangle for proper alpha handling
graphics.Clear(Color(0, 0, 0, 0)); // Fully transparent background
// Apply DPI scaling for high-resolution displays
const float dpiScale = GetDpiScale();
const float radius = kIndicatorRadius * dpiScale;
const float strokeWidth = kStrokeWidth * dpiScale;
// Get system accent color for theming consistency
DWORD accentColor = 0;
BOOL isOpaque = FALSE;
Color progressColor;
Color backgroundCircleColor;
if (SUCCEEDED(DwmGetColorizationColor(&accentColor, &isOpaque)))
{
// Extract RGB components from system accent color
const BYTE a = 255;
const BYTE r = (accentColor >> 16) & 0xFF;
const BYTE g = (accentColor >> 8) & 0xFF;
const BYTE b = accentColor & 0xFF;
progressColor = Color(a, r, g, b);
// Create subtle background color
const BYTE bgR = static_cast<BYTE>((r + 128) / 2);
const BYTE bgG = static_cast<BYTE>((g + 128) / 2);
const BYTE bgB = static_cast<BYTE>((b + 128) / 2);
backgroundCircleColor = Color(80, bgR, bgG, bgB);
}
else
{
// Fallback colors
progressColor = Color(255, 0, 120, 215);
backgroundCircleColor = Color(80, 160, 160, 160);
}
// Create bounding rectangle for the circle
RectF ellipseRect(
centerX - radius,
centerY - radius,
radius * 2,
radius * 2
);
// Draw background circle
Pen bgPen(backgroundCircleColor, strokeWidth * 0.6f);
graphics.DrawEllipse(&bgPen, ellipseRect);
// Draw progress arc only if we have measurable progress
if (m_progress > 0.0f)
{
Pen progressPen(progressColor, strokeWidth);
progressPen.SetStartCap(LineCapRound);
progressPen.SetEndCap(LineCapRound);
const float startAngle = -90.0f; // 12 o'clock position
const float sweepAngle = m_progress * 360.0f;
graphics.DrawArc(&progressPen, ellipseRect, startAngle, sweepAngle);
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: DRAW Arc (Fallback) - Progress %.3f, SweepAngle %.1f degrees\n",
m_progress, sweepAngle);
OutputDebugStringA(debugMsg);
}
else
{
OutputDebugStringA("DwellIndicator: DRAW (Fallback) - No arc (progress 0.0)\n");
}
}
/**
* @brief Hide the indicator window
*
* Makes the window invisible but keeps it alive for potential re-showing.
* Also resets the progress state to ensure clean restart on next show.
*/
void DwellIndicatorImpl::Hide()
{
if (m_hwnd && m_isVisible)
{
char debugMsg[256];
sprintf_s(debugMsg, sizeof(debugMsg),
"DwellIndicator: HIDE - Progress %.3f->0.0, Visible: %s->FALSE\n",
m_progress, m_isVisible ? "TRUE" : "FALSE");
OutputDebugStringA(debugMsg);
// **GDI+ EXPERT FIX: Proper layered window hiding**
// Clear the layered window content before hiding to prevent artifacts
RECT rect;
GetClientRect(m_hwnd, &rect);
int windowSize = rect.right - rect.left;
if (windowSize > 0)
{
HDC screenDC = GetDC(NULL);
HDC memoryDC = CreateCompatibleDC(screenDC);
// Create transparent bitmap
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = windowSize;
bmi.bmiHeader.biHeight = -windowSize;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* pvBits = nullptr;
HBITMAP hBitmap = CreateDIBSection(screenDC, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0);
if (hBitmap && memoryDC)
{
HBITMAP oldBitmap = static_cast<HBITMAP>(SelectObject(memoryDC, hBitmap));
// Clear to fully transparent
// Fix for C26451: Use safe arithmetic to prevent overflow
const size_t bitmapSizeBytes = static_cast<size_t>(windowSize) * static_cast<size_t>(windowSize) * 4ULL;
memset(pvBits, 0, bitmapSizeBytes);
// Update layered window with transparent content
POINT ptSrc = {0, 0};
POINT ptDst = {m_currentX - windowSize/2, m_currentY - windowSize/2};
SIZE size = {windowSize, windowSize};
BLENDFUNCTION blend = {};
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 0; // Make completely transparent
blend.AlphaFormat = AC_SRC_ALPHA;
UpdateLayeredWindow(m_hwnd, screenDC, &ptDst, &size,
memoryDC, &ptSrc, 0, &blend, ULW_ALPHA);
SelectObject(memoryDC, oldBitmap);
DeleteObject(hBitmap);
}
DeleteDC(memoryDC);
ReleaseDC(NULL, screenDC);
}
// Now hide the window
ShowWindow(m_hwnd, SW_HIDE);
m_isVisible = false;
// **CRITICAL: Reset progress when hiding to ensure clean state for next show**
m_progress = 0.0f;
OutputDebugStringA("DwellIndicator: HIDE Complete - Layered window cleared and hidden\n");
}
else
{
OutputDebugStringA("DwellIndicator: HIDE Skipped - already hidden or invalid window\n");
}
}
/**
* @brief Clean up all indicator resources
*
* Hides and destroys the window, shuts down GDI+.
* Called during module shutdown or when indicator is no longer needed.
*/
void DwellIndicatorImpl::Cleanup()
{
Hide(); // Hide window first
// Destroy the window and clean up Windows resources
if (m_hwnd)
{
DestroyWindow(m_hwnd);
m_hwnd = NULL;
}
// Shutdown GDI+ graphics system
if (m_gdiplusToken != 0)
{
GdiplusShutdown(m_gdiplusToken);
m_gdiplusToken = 0;
}
}
/**
* @brief Get DPI scaling factor for the current display
*
* @return DPI scale factor (1.0 = 96 DPI, 1.25 = 120 DPI, etc.)
*/
float DwellIndicatorImpl::GetDpiScale() const
{
if (!m_hwnd) return 1.0f; // Default scale if no window
return static_cast<float>(GetDpiForWindow(m_hwnd)) / 96.0f;
}
// ============================================================================
// DwellIndicator Public Interface Implementation
// ============================================================================
/**
* @brief Constructor - creates the implementation instance
*/
DwellIndicator::DwellIndicator() : m_impl(std::make_unique<DwellIndicatorImpl>())
{
}
/**
* @brief Destructor - ensures cleanup of resources
*/
DwellIndicator::~DwellIndicator()
{
if (m_impl)
{
m_impl->Cleanup();
}
}
/**
* @brief Initialize the indicator system
* @return true if successful, false on failure
*/
bool DwellIndicator::Initialize()
{
return m_impl ? m_impl->Initialize() : false;
}
/**
* @brief Show indicator at cursor position
* @param x Cursor X coordinate
* @param y Cursor Y coordinate
*/
void DwellIndicator::Show(int x, int y)
{
if (m_impl) m_impl->Show(x, y);
}
/**
* @brief Update countdown progress
* @param progress Progress from 0.0 to 1.0
*/
void DwellIndicator::UpdateProgress(float progress)
{
if (m_impl) m_impl->UpdateProgress(progress);
}
/**
* @brief Hide the indicator
*/
void DwellIndicator::Hide()
{
if (m_impl) m_impl->Hide();
}
/**
* @brief Clean up all resources
*/
void DwellIndicator::Cleanup()
{
if (m_impl) m_impl->Cleanup();
}

View File

@@ -0,0 +1,125 @@
/**
* @file DwellIndicator.h
* @brief Visual countdown indicator for DwellCursor module
*
* This header defines the interface for the visual feedback system that shows
* users when a dwell click is about to occur. The indicator appears as a
* circular progress arc that fills clockwise during the countdown period.
*
* Key Features:
* - Transparent overlay window that doesn't interfere with normal interaction
* - System accent color theming for consistency with Windows
* - DPI-aware rendering for high-resolution displays
* - Smooth progress animation updated at 30 FPS
* - Automatic positioning centered on cursor location
*/
#pragma once
#include <memory>
// Forward declaration to hide implementation details (Pimpl idiom)
class DwellIndicatorImpl;
/**
* @brief Visual countdown indicator for dwell cursor functionality
*
* This class provides a clean interface for showing a circular progress
* indicator during dwell cursor countdown. It uses the Pimpl (Pointer to
* Implementation) idiom to hide all Windows/GDI+ dependencies from the header.
*
* Usage Pattern:
* 1. Create instance: DwellIndicator indicator;
* 2. Initialize: indicator.Initialize();
* 3. Show at cursor: indicator.Show(x, y);
* 4. Update progress: indicator.UpdateProgress(0.5f); // 50% complete
* 5. Hide when done: indicator.Hide();
* 6. Cleanup: indicator.Cleanup(); // or let destructor handle it
*
* Thread Safety:
* - All methods must be called from the same thread (UI thread)
* - Progress updates can be called frequently (30ms intervals recommended)
* - Hide/Show calls are safe to call multiple times
*/
class DwellIndicator
{
public:
/**
* @brief Constructor - creates implementation instance
*
* Note: This only creates the object, call Initialize() before use.
*/
DwellIndicator();
/**
* @brief Destructor - ensures proper cleanup
*
* Automatically calls Cleanup() if not already called.
*/
~DwellIndicator();
/**
* @brief Initialize the indicator system
*
* Must be called before any other operations. Sets up:
* - GDI+ graphics system
* - Transparent overlay window
* - DPI awareness
*
* @return true if initialization successful, false on failure
*/
bool Initialize();
/**
* @brief Show the indicator at specified screen coordinates
*
* Displays the circular indicator centered on the given position.
* If already visible, moves to new position. Window is sized
* automatically based on DPI and indicator radius.
*
* @param x Screen X coordinate in pixels
* @param y Screen Y coordinate in pixels
*/
void Show(int x, int y);
/**
* @brief Update the countdown progress
*
* Updates the progress arc to show how much of the dwell delay
* has elapsed. Can be called frequently for smooth animation.
*
* @param progress Progress value from 0.0 (start) to 1.0 (complete)
* Values outside this range are automatically clamped
*/
void UpdateProgress(float progress);
/**
* @brief Hide the indicator
*
* Makes the indicator invisible but keeps resources allocated
* for potential re-showing. Safe to call multiple times.
*/
void Hide();
/**
* @brief Clean up all resources
*
* Destroys the window, shuts down GDI+, releases all resources.
* Called automatically by destructor if not called explicitly.
* After calling this, Initialize() must be called again before reuse.
*/
void Cleanup();
// Disable copy constructor and assignment operator
// The indicator manages Windows resources that shouldn't be copied
DwellIndicator(const DwellIndicator&) = delete;
DwellIndicator& operator=(const DwellIndicator&) = delete;
private:
/**
* @brief Pointer to implementation (Pimpl idiom)
*
* This hides all Windows/GDI+ implementation details from the header,
* reducing compile dependencies and keeping the interface clean.
*/
std::unique_ptr<DwellIndicatorImpl> m_impl;
};

View File

@@ -0,0 +1,595 @@
#include "pch.h"
#include <common/SettingsAPI/settings_objects.h>
#include <interface/powertoy_module_interface.h>
#include "trace.h"
#include <atomic>
#include <thread>
#include <common/utils/logger_helper.h>
#include "DwellIndicator.h"
extern "C" IMAGE_DOS_HEADER __ImageBase;
namespace
{
// JSON configuration keys for settings persistence
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_DELAY_TIME_MS[] = L"delay_time_ms";
const wchar_t JSON_KEY_SETTLE_TIME_SECONDS[] = L"settle_time_seconds";
// Update interval for the visual indicator (in milliseconds)
// 30ms gives ~33 FPS for smooth animation without excessive CPU usage
constexpr DWORD kIndicatorUpdateIntervalMs = 30;
}
/**
* @brief Send a left mouse click via Windows input system
*
* Simulates a complete left click (down + up) at the current cursor position.
* This is the core functionality that gets triggered after the dwell delay.
*/
static void SendLeftClick()
{
INPUT inputs[2]{};
// First input: Left mouse button down
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
// Second input: Left mouse button up
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
// Send both inputs to simulate a complete click
SendInput(2, inputs, sizeof(INPUT));
}
/**
* @brief Main DwellCursor PowerToy module implementation
*
* This class implements the dwell cursor functionality:
* - Monitors mouse movement continuously
* - Detects when mouse becomes stationary
* - Shows visual countdown indicator
* - Triggers left click after configured delay
* - Provides hotkey toggle for enable/disable
*
* State Management:
* - m_enabled: Whether module is active (controlled by PowerToys settings)
* - m_armed: Whether dwell clicking is currently armed (toggled by hotkey)
* - firedForThisStationary: Prevents multiple clicks during one stationary period
*/
class DwellCursorModule : public PowertoyModuleIface
{
private:
// Core module state - SINGLE DECLARATIONS ONLY
bool m_enabled{ false }; // Module enabled/disabled state
Hotkey m_activationHotkey{}; // Hotkey for toggling armed state
// Configuration settings - SINGLE DECLARATIONS ONLY
std::atomic<int> m_delayMs{ 1000 }; // Dwell delay in milliseconds (500-10000ms)
std::atomic<int> m_settleTimeSeconds{ 1 }; // Settle time in seconds (1-5s)
// Runtime state management - SINGLE DECLARATIONS ONLY
std::atomic<bool> m_armed{ true }; // Whether dwell clicking is armed
std::atomic<bool> m_stop{ false }; // Signal to stop the worker thread
std::thread m_worker; // Background thread for mouse monitoring
// Visual feedback system
std::unique_ptr<DwellIndicator> m_indicator;
// Progress tracking - Use member variable instead of static
float m_lastProgress{ -1.0f }; // Last progress value for change detection
// Mouse movement sensitivity (pixels) - SINGLE DECLARATION ONLY
// Movement within this threshold is considered "stationary"
static constexpr int kMoveThresholdPx = 5;
public:
/**
* @brief Constructor - Initialize the DwellCursor module
*
* Sets up logging, loads settings, configures default hotkey,
* and creates the visual indicator instance.
*/
DwellCursorModule()
{
// Initialize logging system for debugging and telemetry
LoggerHelpers::init_logger(L"DwellCursor", L"ModuleInterface", "dwell-cursor");
Logger::trace(L"DwellCursor: Constructor called");
// Load saved settings from PowerToys configuration
init_settings();
// Set default hotkey if not configured: Win+Alt+D
if (m_activationHotkey.key == 0)
{
m_activationHotkey.win = true; // Windows key required
m_activationHotkey.alt = true; // Alt key required
m_activationHotkey.key = 'D'; // D key
}
// Create visual indicator instance (but don't initialize yet)
m_indicator = std::make_unique<DwellIndicator>();
Logger::trace(L"DwellCursor: Constructor completed");
}
/**
* @brief Destructor cleanup
*/
virtual void destroy() override
{
disable(); // Stop all activity
delete this;
}
// PowerToy identification methods
virtual const wchar_t* get_name() override { return L"DwellCursor"; }
virtual const wchar_t* get_key() override { return L"DwellCursor"; }
/**
* @brief Get module configuration for PowerToys settings UI
*/
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
return settings.serialize_to_buffer(buffer, buffer_size);
}
/**
* @brief Apply new configuration from PowerToys settings UI
*/
virtual void set_config(const wchar_t* config) override
{
try
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_settings(values);
}
catch (...)
{
// Ignore configuration errors to prevent crashes
}
}
virtual void call_custom_action(const wchar_t* /*action*/) override {}
/**
* @brief Enable the DwellCursor module
*
* This is called when:
* 1. PowerToys starts up (if module is enabled in settings)
* 2. User enables the module via PowerToys settings UI
*
* Actions performed:
* 1. Initialize visual indicator system
* 2. Start background mouse monitoring thread
* 3. Begin dwell detection
*/
virtual void enable() override
{
if (m_enabled)
{
Logger::trace(L"DwellCursor: Already enabled");
return;
}
Logger::trace(L"DwellCursor: Enabling module");
m_enabled = true;
m_stop = false;
// Initialize the visual indicator system (GDI+, window creation)
if (m_indicator)
{
if (!m_indicator->Initialize())
{
Logger::trace(L"DwellCursor: Failed to initialize visual indicator");
// Continue without visual indicator - core functionality still works
}
else
{
Logger::trace(L"DwellCursor: Visual indicator initialized successfully");
}
}
else
{
Logger::trace(L"DwellCursor: No indicator instance available");
}
// Start the mouse monitoring thread
m_worker = std::thread([this]() { this->RunLoop(); });
Logger::trace(L"DwellCursor: Module enabled and worker thread started");
}
/**
* @brief Disable the DwellCursor module
*
* This is called when:
* 1. PowerToys shuts down
* 2. User disables the module via PowerToys settings UI
*
* Actions performed:
* 1. Stop mouse monitoring thread
* 2. Hide any visible indicator
* 3. Clean up visual indicator resources
*/
virtual void disable() override
{
if (!m_enabled)
{
Logger::trace(L"DwellCursor: Already disabled");
return;
}
Logger::trace(L"DwellCursor: Disabling module");
m_enabled = false;
m_stop = true;
// Wait for worker thread to finish
if (m_worker.joinable()) m_worker.join();
// Clean up visual indicator resources
if (m_indicator)
{
m_indicator->Cleanup();
}
Logger::trace(L"DwellCursor: Module disabled");
}
virtual bool is_enabled() override { return m_enabled; }
virtual bool is_enabled_by_default() const override { return false; } // User must explicitly enable
/**
* @brief Report hotkeys to PowerToys for registration
*/
virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
{
if (buffer && buffer_size >= 1)
{
buffer[0] = m_activationHotkey;
}
return 1; // We have exactly one hotkey
}
/**
* @brief Handle hotkey press events
*
* The hotkey toggles the "armed" state:
* - Armed: Dwell clicking is active, countdown indicator shows
* - Disarmed: No dwell clicking, indicator hidden
*
* This allows users to temporarily disable dwell clicking without
* going into settings (e.g., when typing or doing precise work).
*
* @param hotkeyId Index of the pressed hotkey (we only have one)
* @return true if handled, false otherwise
*/
virtual bool on_hotkey(size_t hotkeyId) override
{
// Handle our single registered hotkey
if (hotkeyId == 0)
{
// Toggle armed state (enabled/disabled functionality)
m_armed = !m_armed.load();
// Hide indicator immediately when disarming or when module disabled
if ((!m_armed || !m_enabled) && m_indicator)
{
m_indicator->Hide();
}
Logger::trace(L"DwellCursor: Hotkey pressed, armed={}, enabled={}", m_armed.load(), m_enabled);
return true;
}
return false;
}
private:
/**
* @brief Load settings from PowerToys configuration files
*/
void init_settings()
{
try
{
PowerToysSettings::PowerToyValues settings = PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
parse_settings(settings);
}
catch (...)
{
// Use default settings if loading fails
}
}
/**
* @brief Parse and apply settings from JSON configuration
*
* Extracts:
* - Activation hotkey configuration
* - Dwell delay time (with validation)
*/
void parse_settings(PowerToysSettings::PowerToyValues& settings)
{
auto obj = settings.get_raw_json();
if (!obj.GetView().Size()) return;
// Parse hotkey configuration
try
{
auto jsonHotkey = obj.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hk = PowerToysSettings::HotkeyObject::from_json(jsonHotkey);
m_activationHotkey = {};
m_activationHotkey.win = hk.win_pressed();
m_activationHotkey.ctrl = hk.ctrl_pressed();
m_activationHotkey.shift = hk.shift_pressed();
m_activationHotkey.alt = hk.alt_pressed();
m_activationHotkey.key = static_cast<unsigned char>(hk.get_code());
}
catch (...)
{
// Keep default hotkey if parsing fails
}
// Parse dwell delay setting
try
{
auto jsonDelay = obj.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_DELAY_TIME_MS);
int v = static_cast<int>(jsonDelay.GetNamedNumber(JSON_KEY_VALUE));
// Validate delay range: 0.5 seconds to 10 seconds
if (v < 500) v = 500; // Minimum 0.5 seconds
if (v > 10000) v = 10000; // Maximum 10 seconds
m_delayMs = v;
}
catch (...)
{
// Keep default delay if parsing fails
}
// Parse settle time setting
try
{
auto jsonSettleTime = obj.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SETTLE_TIME_SECONDS);
int v = static_cast<int>(jsonSettleTime.GetNamedNumber(JSON_KEY_VALUE));
// Validate settle time range: 1 to 5 seconds
if (v < 1) v = 1; // Minimum 1 second
if (v > 5) v = 5; // Maximum 5 seconds
m_settleTimeSeconds = v;
}
catch (...)
{
// Keep default settle time if parsing fails
}
}
/**
* @brief Check if two points are within movement threshold
*
* @param a First coordinate
* @param b Second coordinate
* @param thr Threshold in pixels
* @return true if coordinates are within threshold (considered "near")
*/
static bool Near(int a, int b, int thr) { return (abs(a - b) <= thr); }
/**
* @brief Main mouse monitoring loop (runs in background thread)
*
* This is the core logic that runs continuously while the module is enabled:
*
* State Machine:
* 1. Monitor mouse position every 50ms (20 Hz)
* 2. If mouse moves > threshold: Reset timer, hide indicator
* 3. If mouse stationary for SETTLE_TIME: Show indicator
* 4. If mouse stationary for SETTLE_TIME + dwell delay: Send click
*
* CRITICAL: This method handles ALL progress reset logic
*/
void RunLoop()
{
constexpr DWORD ACTIVE_POLL_INTERVAL = 50; // 50ms = 20 Hz monitoring when active
constexpr DWORD INACTIVE_POLL_INTERVAL = 200; // 200ms when disabled
// Initialize tracking variables
POINT last{}; // Last recorded mouse position
GetCursorPos(&last); // Get initial position
DWORD lastMove = GetTickCount(); // Time of last movement
bool firedForThisStationary = false; // Prevents multiple clicks during one stationary period
bool indicatorShown = false; // Current indicator visibility state
DWORD lastIndicatorUpdate = 0; // Last time we updated indicator progress
Logger::trace(L"DwellCursor: RunLoop started with 50ms polling and {}s configurable settle time, enabled={}, armed={}",
m_settleTimeSeconds.load(), m_enabled, m_armed.load());
// Main monitoring loop - continues until module shutdown
while (!m_stop)
{
// Performance optimization: When module disabled, sleep longer and skip processing
if (!m_enabled)
{
// Hide any visible indicator when disabled
if (indicatorShown && m_indicator)
{
m_indicator->Hide();
indicatorShown = false;
m_lastProgress = -1.0f; // Reset progress tracking
}
Sleep(INACTIVE_POLL_INTERVAL); // Sleep 200ms when disabled
continue;
}
// Get current mouse position and time
POINT p{};
GetCursorPos(&p);
DWORD currentTime = GetTickCount();
// Check if mouse has moved beyond our threshold
if (!Near(p.x, last.x, kMoveThresholdPx) || !Near(p.y, last.y, kMoveThresholdPx))
{
// MOUSE MOVEMENT DETECTED - RESET ALL STATE
Logger::trace(L"DwellCursor: Mouse movement detected, resetting state");
// Update tracking variables
last = p; // Record new position
lastMove = currentTime; // Record movement time
firedForThisStationary = false; // Re-arm for next stationary period
// CRITICAL: Hide indicator and reset progress immediately on movement
if (indicatorShown && m_indicator)
{
m_indicator->Hide();
indicatorShown = false;
}
// Reset progress tracking for next stationary period
m_lastProgress = -1.0f;
}
else
{
// MOUSE IS STATIONARY - Process dwell logic
// Check if we should process dwell logic
if (m_enabled && m_armed && !firedForThisStationary)
{
// Calculate how long mouse has been stationary
DWORD elapsed = currentTime - lastMove;
DWORD delayMs = static_cast<DWORD>(m_delayMs.load());
DWORD settleTimeMs = static_cast<DWORD>(m_settleTimeSeconds.load() * 1000); // Convert seconds to milliseconds
DWORD totalTimeRequired = settleTimeMs + delayMs; // Settle time + dwell delay
if (elapsed >= totalTimeRequired)
{
// SETTLE TIME + DWELL DELAY COMPLETED - TRIGGER CLICK
Logger::trace(L"DwellCursor: Triggering click after {}ms total ({}ms settle + {}ms dwell)", elapsed, settleTimeMs, delayMs);
// Hide indicator before clicking
if (indicatorShown && m_indicator)
{
m_indicator->Hide();
indicatorShown = false;
}
// Reset progress tracking
m_lastProgress = -1.0f;
SendLeftClick(); // Send the mouse click
firedForThisStationary = true; // Prevent additional clicks
}
else if (elapsed >= settleTimeMs)
{
// SETTLE TIME COMPLETED - START/UPDATE COUNTDOWN INDICATOR
DWORD dwellElapsed = elapsed - settleTimeMs; // Time since settle completed
// Show indicator if not already visible
if (!indicatorShown && m_indicator)
{
Logger::trace(L"DwellCursor: Settle time ({}ms) completed, showing NEW indicator at ({}, {}) - dwellElapsed={}ms, delayMs={}",
settleTimeMs, p.x, p.y, dwellElapsed, delayMs);
// CRITICAL: Force complete reset before showing
m_lastProgress = -1.0f;
m_indicator->Show(p.x, p.y); // This internally resets indicator progress to 0.0
indicatorShown = true;
lastIndicatorUpdate = currentTime; // Reset update timer when showing
}
// Update indicator progress ONLY at throttled intervals
if (indicatorShown && (currentTime - lastIndicatorUpdate >= ACTIVE_POLL_INTERVAL))
{
// Calculate progress as percentage: 0.0 = just started, 1.0 = almost complete
float newProgress = static_cast<float>(dwellElapsed) / static_cast<float>(delayMs);
if (newProgress > 1.0f) newProgress = 1.0f; // Clamp to prevent over-draw
// Only update if progress changed significantly (at least 3% or 0.03) OR forced reset
if (abs(newProgress - m_lastProgress) >= 0.03f || m_lastProgress < 0.0f)
{
Logger::trace(L"DwellCursor: Updating progress from {:.2f} to {:.2f} (dwellElapsed={}ms)",
m_lastProgress, newProgress, dwellElapsed);
if (m_indicator)
{
m_indicator->UpdateProgress(newProgress);
}
m_lastProgress = newProgress;
lastIndicatorUpdate = currentTime;
}
}
}
// else: Still in settle time, do nothing (no indicator shown)
}
else if (indicatorShown && m_indicator)
{
// STATIONARY BUT CONDITIONS NOT MET - HIDE INDICATOR
// This happens when: not enabled, not armed, or already fired
Logger::trace(L"DwellCursor: Hiding indicator - conditions not met (enabled={}, armed={}, fired={})",
m_enabled, m_armed.load(), firedForThisStationary);
m_indicator->Hide();
indicatorShown = false;
// Reset progress tracking
m_lastProgress = -1.0f;
}
}
// Sleep appropriate interval based on activity (20 Hz when active)
Sleep(ACTIVE_POLL_INTERVAL);
}
// THREAD SHUTDOWN CLEANUP
Logger::trace(L"DwellCursor: RunLoop shutdown - cleaning up");
// Hide indicator when stopping
if (indicatorShown && m_indicator)
{
m_indicator->Hide();
}
// Final progress reset
m_lastProgress = -1.0f;
Logger::trace(L"DwellCursor: RunLoop ended");
}
};
// ============================================================================
// DLL Entry Points
// ============================================================================
/**
* @brief DLL entry point for Windows module loading
*/
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider(); // Initialize ETW tracing
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider(); // Cleanup ETW tracing
break;
}
return TRUE;
}
/**
* @brief PowerToys module factory function
*
* This is the entry point called by PowerToys runner to create an instance
* of our module. The runner will call this once during startup.
*
* @return New instance of DwellCursorModule
*/
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new DwellCursorModule();
}

View File

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

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,20 @@
#pragma once
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <ShellScalingApi.h>
#include <dwmapi.h>
#include <stdint.h>
#include <atomic>
#include <thread>
#include <chrono>
#include <optional>
#include <string>
#include <memory>
#include <algorithm>
#include <cmath>
#include <common/SettingsAPI/settings_objects.h>
#include <interface/powertoy_module_interface.h>
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>

View File

@@ -0,0 +1,2 @@
#pragma once
#define IDS_MODULE_NAME 1001

View File

@@ -0,0 +1,2 @@
#include "pch.h"
#include "trace.h"

View File

@@ -0,0 +1,2 @@
#pragma once
namespace Trace { inline void RegisterProvider(){} inline void UnregisterProvider(){} }

View File

@@ -21,6 +21,26 @@
// Note: Settings are managed via Settings and UI Settings
class NewModule : public PowertoyModuleIface
{
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
Logger::info(L"New+ context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered");
#endif
}
}
public:
NewModule()
{
@@ -98,14 +118,9 @@ public:
{
newplus::utilities::register_msix_package();
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
#endif
}
powertoy_new_enabled = true;
UpdateRegistration(powertoy_new_enabled);
}
virtual void disable() override
@@ -150,19 +165,14 @@ private:
{
Trace::EventToggleOnOff(false);
}
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered (Win10)");
#endif
}
powertoy_new_enabled = false;
UpdateRegistration(powertoy_new_enabled);
}
void init_settings()
{
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
UpdateRegistration(powertoy_new_enabled);
}
};

View File

@@ -49,7 +49,9 @@ namespace Awake.Core
private static DateTimeOffset ExpireAt { get; set; }
private static readonly CompositeFormat AwakeMinute = CompositeFormat.Parse(Resources.AWAKE_MINUTE);
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
private static readonly CompositeFormat AwakeHour = CompositeFormat.Parse(Resources.AWAKE_HOUR);
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
@@ -451,7 +453,7 @@ namespace Awake.Core
Dictionary<string, uint> optionsList = new()
{
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHour, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
};
return optionsList;

View File

@@ -159,6 +159,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} hour.
/// </summary>
internal static string AWAKE_HOUR {
get {
return ResourceManager.GetString("AWAKE_HOUR", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} hours.
/// </summary>
@@ -240,6 +249,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} minute.
/// </summary>
internal static string AWAKE_MINUTE {
get {
return ResourceManager.GetString("AWAKE_MINUTE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} minutes.
/// </summary>

View File

@@ -123,6 +123,10 @@
<data name="AWAKE_EXIT" xml:space="preserve">
<value>Exit</value>
</data>
<data name="AWAKE_HOUR" xml:space="preserve">
<value>{0} hour</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_HOURS" xml:space="preserve">
<value>{0} hours</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@@ -142,6 +146,10 @@
<value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment>
</data>
<data name="AWAKE_MINUTE" xml:space="preserve">
<value>{0} minute</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_MINUTES" xml:space="preserve">
<value>{0} minutes</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>

View File

@@ -0,0 +1,155 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Common.Commands;
public sealed partial class ConfirmableCommand : InvokableCommand
{
private readonly IInvokableCommand? _command;
public Func<bool>? IsConfirmationRequired { get; init; }
public required string ConfirmationTitle { get; init; }
public required string ConfirmationMessage { get; init; }
public required IInvokableCommand Command
{
get => _command!;
init
{
if (_command is INotifyPropChanged oldNotifier)
{
oldNotifier.PropChanged -= InnerCommand_PropChanged;
}
_command = value;
if (_command is INotifyPropChanged notifier)
{
notifier.PropChanged += InnerCommand_PropChanged;
}
OnPropertyChanged(nameof(Name));
OnPropertyChanged(nameof(Id));
OnPropertyChanged(nameof(Icon));
}
}
public override string Name
{
get => (_command as Command)?.Name ?? base.Name;
set
{
if (_command is Command cmd)
{
cmd.Name = value;
}
else
{
base.Name = value;
}
}
}
public override string Id
{
get => (_command as Command)?.Id ?? base.Id;
set
{
var previous = Id;
if (_command is Command cmd)
{
cmd.Id = value;
}
else
{
base.Id = value;
}
if (previous != Id)
{
OnPropertyChanged(nameof(Id));
}
}
}
public override IconInfo Icon
{
get => (_command as Command)?.Icon ?? base.Icon;
set
{
if (_command is Command cmd)
{
cmd.Icon = value;
}
else
{
base.Icon = value;
}
}
}
public ConfirmableCommand()
{
// Allow init-only construction
}
[SetsRequiredMembers]
public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func<bool>? isConfirmationRequired = null)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage);
ArgumentNullException.ThrowIfNull(confirmationMessage);
IsConfirmationRequired = isConfirmationRequired;
ConfirmationTitle = confirmationTitle;
ConfirmationMessage = confirmationMessage;
Command = command;
}
private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args)
{
var property = args.PropertyName;
if (string.IsNullOrEmpty(property) || property == nameof(Name))
{
OnPropertyChanged(nameof(Name));
}
if (string.IsNullOrEmpty(property) || property == nameof(Id))
{
OnPropertyChanged(nameof(Id));
}
if (string.IsNullOrEmpty(property) || property == nameof(Icon))
{
OnPropertyChanged(nameof(Icon));
}
}
public override ICommandResult Invoke()
{
var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true;
if (showConfirmationDialog)
{
return CommandResult.Confirm(new ConfirmationArgs
{
Title = ConfirmationTitle,
Description = ConfirmationMessage,
PrimaryCommand = Command,
IsPrimaryCommandCritical = true,
});
}
else
{
return Command.Invoke(this) ?? CommandResult.Dismiss();
}
}
}

View File

@@ -0,0 +1,51 @@
// 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;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Well-known key chords used in the Command Palette and extensions.
/// </summary>
/// <remarks>
/// Assigned key chords should not conflict with system or application shortcuts.
/// However, the key chords in this class are not guaranteed to be unique and may conflict
/// with each other, especially when commands appear together in the same menu.
/// </remarks>
public static class WellKnownKeyChords
{
/// <summary>
/// Gets the well-known key chord for opening the file location. Shortcut: Ctrl+Shift+E.
/// </summary>
public static KeyChord OpenFileLocation { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.E);
/// <summary>
/// Gets the well-known key chord for copying the file path. Shortcut: Ctrl+Shift+C.
/// </summary>
public static KeyChord CopyFilePath { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.C);
/// <summary>
/// Gets the well-known key chord for opening the current location in a console. Shortcut: Ctrl+Shift+R.
/// </summary>
public static KeyChord OpenInConsole { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.R);
/// <summary>
/// Gets the well-known key chord for running the selected item as administrator. Shortcut: Ctrl+Shift+Enter.
/// </summary>
public static KeyChord RunAsAdministrator { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.Enter);
/// <summary>
/// Gets the well-known key chord for running the selected item as a different user. Shortcut: Ctrl+Shift+U.
/// </summary>
public static KeyChord RunAsDifferentUser { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.U);
/// <summary>
/// Gets the well-known key chord for toggling the pin state. Shortcut: Ctrl+P.
/// </summary>
public static KeyChord TogglePin { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: (int)VirtualKey.P);
}

View File

@@ -160,7 +160,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Initialized |= InitializedState.Initialized;
}
public void SlowInitializeProperties()
public virtual void SlowInitializeProperties()
{
if (IsSelectedInitialized)
{

View File

@@ -47,9 +47,21 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateTags(li.Tags);
TextToSuggest = li.TextToSuggest;
Section = li.Section ?? string.Empty;
var extensionDetails = li.Details;
UpdateProperty(nameof(Section));
}
public override void SlowInitializeProperties()
{
base.SlowInitializeProperties();
var model = Model.Unsafe;
if (model is null)
{
return;
}
var extensionDetails = model.Details;
if (extensionDetails is not null)
{
Details = new(extensionDetails, PageContext);
@@ -58,8 +70,8 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails));
}
TextToSuggest = model.TextToSuggest;
UpdateProperty(nameof(TextToSuggest));
UpdateProperty(nameof(Section));
}
protected override void FetchProperty(string propertyName)

View File

@@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -39,7 +41,7 @@ public partial class AppStateModel : ObservableObject
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadState)}");
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
}
if (!File.Exists(FilePath))
@@ -77,43 +79,84 @@ public partial class AppStateModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
// validate JSON
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
{
// Now, read the existing content from the file
var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}";
Logger.LogError("Failed to parse app state as a JsonObject.");
return;
}
// Is it valid JSON?
if (JsonNode.Parse(oldContent) is JsonObject savedSettings)
{
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
// read previous settings
if (!TryReadSavedState(out var savedSettings))
{
savedSettings = new JsonObject();
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
// merge new settings into old ones
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
// TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and
// reload the settings then
model.StateChanged?.Invoke(model, null);
}
else
{
Debug.WriteLine("Failed to parse settings file as JsonObject.");
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and
// reload the settings then
model.StateChanged?.Invoke(model, null);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
}
}
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
{
savedSettings = null;
// read existing content from the file
string oldContent;
try
{
if (File.Exists(FilePath))
{
oldContent = File.ReadAllText(FilePath);
}
else
{
Debug.WriteLine("Failed to parse settings file as JsonObject.");
// file doesn't exist (might not have been created yet), so consider this a success
// and return empty settings
savedSettings = new JsonObject();
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
return false;
}
// detect empty file, just for sake of logging
if (string.IsNullOrWhiteSpace(oldContent))
{
Logger.LogInfo($"App state file is empty: {FilePath}");
return false;
}
// is it valid JSON?
try
{
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
return savedSettings != null;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
return false;
}
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,6 +20,10 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem
Title = string.Empty;
_logMessagesPage.Name = string.Empty;
Subtitle = Properties.Resources.builtin_log_subtitle;
var logPath = Logger.LogDirectoryPath("\\CmdPal\\Logs\\");
var openLogCommand = new OpenFileCommand(logPath) { Name = Resources.builtin_log_folder_command_name };
MoreCommands = [new CommandContextItem(openLogCommand)];
}
public override void UpdateQuery(string query)

View File

@@ -27,7 +27,9 @@ public partial class MainListPage : DynamicListPage,
private readonly IServiceProvider _serviceProvider;
private readonly TopLevelCommandManager _tlcManager;
private IEnumerable<IListItem>? _filteredItems;
private IEnumerable<Scored<IListItem>>? _filteredItems;
private IEnumerable<Scored<IListItem>>? _filteredApps;
private IEnumerable<IListItem>? _allApps;
private bool _includeApps;
private bool _filteredItemsIncludesApps;
@@ -83,7 +85,7 @@ public partial class MainListPage : DynamicListPage,
}
else
{
RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
RaiseItemsChanged();
}
}
@@ -148,7 +150,13 @@ public partial class MainListPage : DynamicListPage,
{
lock (_tlcManager.TopLevelCommands)
{
return _filteredItems?.ToArray() ?? [];
var items = Enumerable.Empty<Scored<IListItem>>()
.Concat(_filteredItems is not null ? _filteredItems : [])
.Concat(_filteredApps is not null ? _filteredApps : [])
.OrderByDescending(o => o.Score)
.Select(s => s.Item)
.ToArray();
return items;
}
}
}
@@ -167,6 +175,8 @@ public partial class MainListPage : DynamicListPage,
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
}
@@ -184,6 +194,8 @@ public partial class MainListPage : DynamicListPage,
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
_filteredApps = null;
_allApps = null;
RaiseItemsChanged(commands.Count);
return;
}
@@ -193,35 +205,49 @@ public partial class MainListPage : DynamicListPage,
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
{
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
// If the internal state has changed, reset _filteredItems to reset the list.
if (_filteredItemsIncludesApps != _includeApps)
{
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
var newFilteredItems = _filteredItems?.Select(s => s.Item);
// If we don't have any previous filter results to work with, start
// with a list of all our commands & apps.
if (_filteredItems is null)
if (newFilteredItems is null && _filteredApps is null)
{
_filteredItems = commands;
newFilteredItems = commands;
_filteredItemsIncludesApps = _includeApps;
if (_includeApps)
{
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
var appIds = apps.Select(app => app.Command.Id).ToArray();
// Remove any top level pinned apps and use the apps from AllAppsCommandProvider.Page.GetItems()
// since they contain details.
_filteredItems = _filteredItems.Where(item => item.Command is not AppCommand);
_filteredItems = _filteredItems.Concat(apps);
_allApps = AllAppsCommandProvider.Page.GetItems();
}
}
// Produce a list of everything that matches the current filter.
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
RaiseItemsChanged(_filteredItems.Count());
_filteredItems = ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem);
// Produce a list of filtered apps with the appropriate limit
if (_allApps is not null)
{
_filteredApps = ListHelpers.FilterListWithScores<IListItem>(_allApps, SearchText, ScoreTopLevelItem);
var appResultLimit = AllAppsCommandProvider.TopLevelResultLimit;
if (appResultLimit >= 0)
{
_filteredApps = _filteredApps.Take(appResultLimit);
}
}
RaiseItemsChanged();
}
}

View File

@@ -126,6 +126,10 @@ public class ExtensionWrapper : IExtensionWrapper
// We'll just return out nothing.
return;
}
else if (hr.Value != 0)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
}
// Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);

View File

@@ -285,6 +285,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to View log folder.
/// </summary>
public static string builtin_log_folder_command_name {
get {
return ResourceManager.GetString("builtin_log_folder_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to View log.
/// </summary>

View File

@@ -135,6 +135,9 @@
<data name="builtin_log_title" xml:space="preserve">
<value>View log</value>
</data>
<data name="builtin_log_folder_command_name" xml:space="preserve">
<value>View log folder</value>
</data>
<data name="builtin_reload_subtitle" xml:space="preserve">
<value>Reload Command Palette extensions</value>
</data>

View File

@@ -32,6 +32,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private string _generatedId = string.Empty;
private HotkeySettings? _hotkey;
private IIconInfo? _initialIcon;
private CommandAlias? Alias { get; set; }
@@ -57,6 +58,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
public IIconInfo Icon => _commandItemViewModel.Icon;
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
@@ -205,6 +208,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
DisplayTitle = fallback.DisplayTitle;
}
UpdateInitialIcon(false);
}
}
@@ -221,7 +226,31 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
FetchAliasFromAliasManager();
UpdateHotkey();
UpdateTags();
UpdateInitialIcon();
}
else if (e.PropertyName == nameof(CommandItem.Icon))
{
UpdateInitialIcon();
}
}
}
private void UpdateInitialIcon(bool raiseNotification = true)
{
if (_initialIcon != null || !_commandItemViewModel.Icon.IsSet)
{
return;
}
_initialIcon = _commandItemViewModel.Icon;
if (raiseNotification)
{
DoOnUiThread(
() =>
{
PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(InitialIcon)));
});
}
}

View File

@@ -85,7 +85,7 @@ public partial class App : Application
AppWindow = new MainWindow();
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs);
((MainWindow)AppWindow).HandleLaunchNonUI(activatedEventArgs);
}
/// <summary>

View File

@@ -134,6 +134,15 @@ public sealed partial class CommandBar : UserControl,
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
}
/// <summary>
/// Sets focus to the "More" button after closing the context menu,
/// keeping keyboard navigation intuitive.
/// </summary>
public void FocusMoreCommandsButton()
{
MoreCommandsButton?.Focus(FocusState.Programmatic);
}
private void ContextMenuFlyout_Opened(object sender, object e)
{
// We need to wait until our flyout is opened to try and toss focus

View File

@@ -15,6 +15,7 @@
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
Background="Transparent"
PreviewKeyDown="UserControl_PreviewKeyDown"
mc:Ignorable="d">
<UserControl.Resources>

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Messages;
@@ -115,6 +116,24 @@ public sealed partial class ContextMenu : UserControl,
}
}
/// <summary>
/// Handles Escape to close the context menu and return focus to the "More" button.
/// </summary>
private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Escape)
{
// Close the context menu (if not already handled)
WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage());
// Find the parent CommandBar and set focus to MoreCommandsButton
var parent = this.FindParent<CommandBar>();
parent?.FocusMoreCommandsButton();
e.Handled = true;
}
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var prop = e.PropertyName;

View File

@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Deferred;
using Microsoft.Terminal.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -55,6 +57,8 @@ public partial class IconBox : ContentControl
{
TabFocusNavigation = KeyboardNavigationMode.Once;
IsTabStop = false;
HorizontalContentAlignment = HorizontalAlignment.Center;
VerticalContentAlignment = VerticalAlignment.Center;
}
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -75,6 +79,8 @@ public partial class IconBox : ContentControl
IconSourceElement elem = new()
{
IconSource = fontIco,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
@this.Content = elem;
break;
@@ -98,14 +104,20 @@ public partial class IconBox : ContentControl
else
{
// TODO GH #239 switch back when using the new MD text block
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
// _ = @this._queue.EnqueueAsync(() =>
@this._queue.TryEnqueue(new(async () =>
@this._queue.TryEnqueue(async void () =>
{
var requestedTheme = @this.ActualTheme;
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
if (@this.SourceRequested is not null)
try
{
if (@this.SourceRequested is null)
{
return;
}
var requestedTheme = @this.ActualTheme;
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
// After the await:
@@ -130,37 +142,35 @@ public partial class IconBox : ContentControl
// So, if the icon we get back was a font icon,
// and the glyph for that icon is NOT in the range of
// Segoe icons, then let's give the icon some extra space
@this.Padding = new Thickness(0);
IconDataViewModel? iconData = null;
if (eventArgs.Key is IconDataViewModel)
var iconData = eventArgs.Key switch
{
iconData = eventArgs.Key as IconDataViewModel;
IconDataViewModel key => key,
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
_ => null,
};
if (iconData?.Icon is not null && @this.Source is FontIconSource)
{
var iconSize =
!double.IsNaN(@this.Width) ? @this.Width :
!double.IsNaN(@this.Height) ? @this.Height :
@this.ActualWidth > 0 ? @this.ActualWidth :
@this.ActualHeight;
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
}
else if (eventArgs.Key is IconInfoViewModel info)
else
{
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
}
if (iconData is not null &&
@this.Source is FontIconSource)
{
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
{
var ch = iconData.Icon[0];
// The range of MDL2 Icons isn't explicitly defined, but
// we're using this based off the table on:
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
var isMDL2Icon = ch is >= '\uE700' and <= '\uF8FF';
if (!isMDL2Icon)
{
@this.Padding = new Thickness(-4);
}
}
@this.Padding = default;
}
}
}));
catch (Exception ex)
{
// Exception from TryEnqueue bypasses the global error handler,
// and crashes the app.
Logger.LogError("Failed to set icon", ex);
}
});
}
}
}

View File

@@ -123,6 +123,9 @@ public sealed partial class MainWindow : WindowEx,
_localKeyboardListener = new LocalKeyboardListener();
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
_localKeyboardListener.Start();
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
HideWindow();
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
@@ -233,9 +236,6 @@ public sealed partial class MainWindow : WindowEx,
{
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Make sure our HWND is cloaked before any possible window manipulations
Cloak();
// Remember, IsIconic == "minimized", which is entirely different state
// from "show/hide"
// If we're currently minimized, restore us first, before we reveal
@@ -243,6 +243,9 @@ public sealed partial class MainWindow : WindowEx,
// which would remain not visible to the user.
if (PInvoke.IsIconic(hwnd))
{
// Make sure our HWND is cloaked before any possible window manipulations
Cloak();
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
}
@@ -481,8 +484,13 @@ public sealed partial class MainWindow : WindowEx,
}
}
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
{
// LOAD BEARING
// Any reading and processing of the activation arguments must be done
// synchronously in this method, before it returns. The sending instance
// remains blocked until this returns; afterward it may quit, causing
// the activation arguments to be lost.
if (activatedEventArgs is null)
{
Summon(string.Empty);
@@ -519,9 +527,26 @@ public sealed partial class MainWindow : WindowEx,
}
catch (COMException ex)
{
// https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
const int RPC_S_CALL_FAILED = 2147023170;
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
// if the args are not valid or not passed correctly.
Logger.LogError("COM exception when activating the application", ex);
if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
{
Logger.LogWarning(
$"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
$"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
$"The application will continue running and fall back to showing the Command Palette window.");
}
else
{
Logger.LogError(
$"COM exception (HRESULT {ex.HResult}) when activating the application. " +
$"The application will continue running and fall back to showing the Command Palette window.",
ex);
}
}
Summon(string.Empty);
@@ -610,6 +635,20 @@ public sealed partial class MainWindow : WindowEx,
}
private void HandleSummon(string commandId)
{
if (_ignoreHotKeyWhenFullScreen)
{
// If we're in full screen mode, ignore the hotkey
if (WindowHelper.IsWindowFullscreen())
{
return;
}
}
HandleSummonCore(commandId);
}
private void HandleSummonCore(string commandId)
{
var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
@@ -634,8 +673,6 @@ public sealed partial class MainWindow : WindowEx,
// so that we can bind hotkeys to individual commands
if (!isVisible || !isRootHotkey)
{
Activate();
Summon(commandId);
}
else if (isRootHotkey)
@@ -671,15 +708,6 @@ public sealed partial class MainWindow : WindowEx,
var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count)
{
if (_ignoreHotKeyWhenFullScreen)
{
// If we're in full screen mode, ignore the hotkey
if (WindowHelper.IsWindowFullscreen())
{
return (LRESULT)IntPtr.Zero;
}
}
var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId);
}

View File

@@ -107,12 +107,33 @@ internal sealed class Program
{
// Do the redirection on another thread, and use a non-blocking
// wait method to wait for the redirection to complete.
var redirectSemaphore = new Semaphore(0, 1);
Task.Run(() =>
using var redirectSemaphore = new Semaphore(0, 1);
var redirectTimeout = TimeSpan.FromSeconds(32);
_ = Task.Run(() =>
{
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
redirectSemaphore.Release();
using var cts = new CancellationTokenSource(redirectTimeout);
try
{
keyInstance.RedirectActivationToAsync(args)
.AsTask(cts.Token)
.GetAwaiter()
.GetResult();
}
catch (OperationCanceledException)
{
Logger.LogError($"Failed to activate existing instance; timed out after {redirectTimeout}.");
}
catch (Exception ex)
{
Logger.LogError("Failed to activate existing instance", ex);
}
finally
{
redirectSemaphore.Release();
}
});
_ = PInvoke.CoWaitForMultipleObjects(
(uint)CWMO_FLAGS.CWMO_DEFAULT,
PInvoke.INFINITE,
@@ -124,13 +145,14 @@ internal sealed class Program
{
// If we already have a form, display the message now.
// Otherwise, add it to the collection for displaying later.
if (App.Current is App thisApp)
if (App.Current?.AppWindow is MainWindow mainWindow)
{
if (thisApp.AppWindow is not null and
MainWindow mainWindow)
{
uiContext?.Post(_ => mainWindow.HandleLaunch(args), null);
}
// LOAD BEARING
// This must be synchronous to ensure the method does not return
// before the activation is fully handled and the parameters are processed.
// The sending instance remains blocked until this returns; afterward it may quit,
// causing the activation arguments to be lost.
mainWindow.HandleLaunchNonUI(args);
}
}
}

View File

@@ -125,7 +125,7 @@
Width="20"
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceKey="{x:Bind InitialIcon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>

View File

@@ -24,23 +24,15 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- TO DO: Replace this with WinUI TitleBar once that ships. -->
<StackPanel
x:Name="AppTitleBar"
Grid.Row="0"
Height="48"
Margin="16,0,0,0"
Orientation="Horizontal">
<Image
Width="16"
Height="16"
Source="ms-appx:///Assets/icon.svg" />
<TextBlock
x:Uid="CmdPalSettingsHeader"
Margin="12,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
<TitleBar x:Name="TitleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="ms-appx:///Assets/icon.svg" />
</TitleBar.LeftHeader>
</TitleBar>
<NavigationView
x:Name="NavView"
Grid.Row="1"
@@ -77,7 +69,6 @@
x:Name="NavigationBreadcrumbBar"
Grid.Row="0"
MaxWidth="1000"
Margin="16,0,0,0"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>

View File

@@ -31,8 +31,10 @@ public sealed partial class SettingsWindow : WindowEx,
this.InitializeComponent();
this.ExtendsContentIntoTitleBar = true;
this.SetIcon();
this.AppWindow.Title = RS_.GetString("SettingsWindowTitle");
var title = RS_.GetString("SettingsWindowTitle");
this.AppWindow.Title = title;
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
this.TitleBar.Title = title;
PositionCentered();
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);

View File

@@ -438,7 +438,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="NavigationPaneClosed" xml:space="preserve">
<value>Navigation pane closed</value>
</data>
<data name="NavigationPageOpened" xml:space="preserve">
<data name="NavigationPaneOpened" xml:space="preserve">
<value>Navigation page opened</value>
</data>
</root>

View File

@@ -0,0 +1,177 @@
#include "pch.h"
#include "FontIconGlyphClassifier.h"
#include "FontIconGlyphClassifier.g.cpp"
#include <icu.h>
#include <utility>
namespace winrt::Microsoft::Terminal::UI::implementation
{
namespace
{
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
{
static constexpr UChar32 _fluentIconsPrivateUseAreaStart = 0xE700;
static constexpr UChar32 _fluentIconsPrivateUseAreaEnd = 0xF8FF;
return cp >= _fluentIconsPrivateUseAreaStart && cp <= _fluentIconsPrivateUseAreaEnd;
}
// Determine if the given text (as a sequence of UChar code units) is emoji
[[nodiscard]] bool _isEmoji(const UChar* p, const int32_t length) noexcept
{
if (!p || length < 1)
{
return false;
}
// https://www.unicode.org/reports/tr51/#Emoji_Variation_Selector_Notes
constexpr UChar32 vs15CodePoint = 0xFE0E; // Variation Selectors 15: text variation selector
constexpr UChar32 vs16CodePoint = 0xFE0F; // Variation Selectors: 16 emoji variation selector
// Decode the first code point correctly (surrogate-safe)
int32_t i0{ 0 };
UChar32 first{ 0 };
U16_NEXT(p, i0, length, first);
for (int32_t i = 0; i < length;)
{
UChar32 cp{ 0 };
U16_NEXT(p, i, length, cp);
if (cp == vs16CodePoint) { return true; }
if (cp == vs15CodePoint) { return false; }
}
return !U_IS_SURROGATE(first) && u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION);
}
}
bool FontIconGlyphClassifier::IsLikelyToBeEmojiOrSymbolIcon(const hstring& text)
{
if (text.empty())
{
return false;
}
if (text.size() == 1 && !IS_HIGH_SURROGATE(text[0]))
{
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
// If it turns out to be illegal Unicode, we don't really care.
return true;
}
if (text.size() >= 2 && text[0] <= 0x7F && text[1] <= 0x7F)
{
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
// grapheme cluster.
return false;
}
// Use ICU to determine whether text is composed of a single grapheme cluster.
int32_t off{ 0 };
UErrorCode status{ U_ZERO_ERROR };
UBreakIterator* const bi{ ubrk_open(UBRK_CHARACTER,
nullptr,
reinterpret_cast<const UChar*>(text.data()),
static_cast<int>(text.size()),
&status) };
if (bi)
{
if (U_SUCCESS(status))
{
off = ubrk_next(bi);
}
ubrk_close(bi);
}
return std::cmp_equal(off, text.size());
}
FontIconGlyphKind FontIconGlyphClassifier::Classify(hstring const& text) noexcept
{
if (text.empty())
{
return FontIconGlyphKind::None;
}
const size_t textSize{ text.size() };
const auto* buffer{ reinterpret_cast<const UChar*>(text.c_str()) };
// Fast path 1: Single UTF-16 code unit (most common case)
if (textSize == 1)
{
const UChar ch{ buffer[0] };
if (IS_HIGH_SURROGATE(ch))
{
return FontIconGlyphKind::Invalid;
}
if (_isFluentIconPua(ch))
{
return FontIconGlyphKind::FluentSymbol;
}
if (_isEmoji(&ch, 1))
{
return FontIconGlyphKind::Emoji;
}
return FontIconGlyphKind::Other;
}
// Fast path 2: Common file path pattern - two ASCII printable characters
if (textSize >= 2 && buffer[0] <= 0x7F && buffer[1] <= 0x7F)
{
// Definitely multiple graphemes
return FontIconGlyphKind::Invalid;
}
// Expensive path: Use ICU to determine grapheme boundaries
UErrorCode status{ U_ZERO_ERROR };
UBreakIterator* bi{ ubrk_open(UBRK_CHARACTER,
nullptr,
buffer,
static_cast<int32_t>(textSize),
&status) };
if (U_FAILURE(status) || !bi)
{
return FontIconGlyphKind::Invalid;
}
const int32_t start{ ubrk_first(bi) };
const int32_t end{ ubrk_next(bi) }; // end of first grapheme
ubrk_close(bi);
// No graphemes found
if (end == UBRK_DONE || end <= start)
{
return FontIconGlyphKind::None;
}
// If there's more than one grapheme, it's not a valid icon glyph
if (std::cmp_not_equal(end, textSize))
{
return FontIconGlyphKind::Invalid;
}
// Exactly one grapheme: classify
const UChar* grapheme = buffer + start;
const int32_t graphemeLength = end - start;
if (graphemeLength == 1 && _isFluentIconPua(grapheme[0]))
{
return FontIconGlyphKind::FluentSymbol;
}
if (_isEmoji(grapheme, graphemeLength))
{
return FontIconGlyphKind::Emoji;
}
return FontIconGlyphKind::Other;
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "FontIconGlyphClassifier.g.h"
namespace winrt::Microsoft::Terminal::UI::implementation
{
struct FontIconGlyphClassifier
{
[[nodiscard]] static bool IsLikelyToBeEmojiOrSymbolIcon(const winrt::hstring& text);
[[nodiscard]] static FontIconGlyphKind Classify(winrt::hstring const& text) noexcept;
};
}
namespace winrt::Microsoft::Terminal::UI::factory_implementation
{
BASIC_FACTORY(FontIconGlyphClassifier);
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.UI
{
/// <summary>
/// Categorizes the type of a single grapheme cluster or input text.
/// Used to determine how the input should be handled or rendered (for example,
/// whether it should be treated as an emoji, an icon from a symbol font, plain text, etc.).
/// </summary>
enum FontIconGlyphKind
{
/// <summary>
/// Input is invalid or contains more than one grapheme cluster and therefore cannot be
/// treated as a single symbol. Typical for multi-character text like file paths
/// or composed strings that include separators.
/// </summary>
Invalid = -1,
/// <summary>
/// No grapheme present (empty string). Indicates absence of a symbol.
/// </summary>
None = 0,
/// <summary>
/// A single emoji grapheme cluster. This may consist of multiple Unicode code
/// points combined into one visible glyph (e.g., emoji with modifiers or ZWJ sequences).
/// </summary>
Emoji = 1,
/// <summary>
/// A single glyph from the Segoe Fluent Icons / MDL2 Assets Private Use Area (PUA),
/// typically in the Unicode range U+E700U+F8FF. These are font-based icons (Fluent/MDL2).
/// </summary>
FluentSymbol = 2,
/// <summary>
/// A single non-emoji grapheme that is not a Fluent/MDL2 PUA symbol.
/// Covers ordinary characters, letters, numbers, or other single glyph symbols.
/// </summary>
Other = 3,
};
/// <summary>
/// Static utility class for text and icon analysis
/// </summary>
static runtimeclass FontIconGlyphClassifier
{
/// <summary>
/// Determines if text represents a single grapheme cluster (emoji/symbol icon).
/// Uses ICU for Unicode boundary detection to distinguish icons from file paths.
/// </summary>
/// <param name="text">Text to analyze</param>
/// <returns>True if single grapheme cluster, false for multi-character text or paths</returns>
static Boolean IsLikelyToBeEmojiOrSymbolIcon(String text);
/// <summary>
/// Classifies the input into a glyph kind suitable for icon or text rendering.
/// </summary>
static FontIconGlyphKind Classify(String text);
};
}

View File

@@ -2,7 +2,7 @@
#include "IconPathConverter.h"
#include "IconPathConverter.g.cpp"
// #include "Utils.h"
#include "FontIconGlyphClassifier.h"
#include <Shlobj.h>
#include <Shlobj_core.h>
@@ -110,7 +110,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
{
typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
iconSource.ImageSource(source);
return iconSource;
}
@@ -169,41 +169,46 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// If we fail to set the icon source using the "icon" as a path,
// let's try it as a symbol/emoji.
//
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so
// don't do this if it's just an invalid path.
if (!iconSource && iconPath.size() <= 2)
if (!iconSource)
{
try
{
typename FontIconSource<TIconSource>::type icon;
const auto ch = til::at(iconPath, 0);
const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath);
// The range of MDL2 Icons isn't explicitly defined, but
// we're using this based off the table on:
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
const auto isMDL2Icon = ch >= L'\uE700' && ch <= L'\uF8FF';
if (isMDL2Icon)
winrt::hstring family;
if (glyph_kind == FontIconGlyphKind::Invalid)
{
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
family = L"Segoe UI";
}
else if (!fontFamily.empty())
{
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily });
family = fontFamily;
}
else if (glyph_kind == FontIconGlyphKind::FluentSymbol)
{
family = L"Segoe Fluent Icons, Segoe MDL2 Assets";
}
else if (glyph_kind == FontIconGlyphKind::Emoji)
{
// Emoji and other symbols go in the Segoe UI Emoji font.
// Some emojis (e.g. 2⃣) would be rendered as emoji glyphs otherwise.
family = L"Segoe UI Emoji, Segoe UI";
}
else
{
// Note: you _do_ need to manually set the font here.
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe UI" });
family = L"Segoe UI";
}
typename FontIconSource<TIconSource>::type icon;
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
icon.FontSize(targetSize);
icon.Glyph(iconPath);
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
iconSource = icon;
}
CATCH_LOG();
}
}
if (!iconSource)
{
// Set the default IconSource to a BitmapIconSource with a null source
@@ -326,7 +331,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
}
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
int index,
int index,
int targetSize)
{
// Try:

View File

@@ -159,6 +159,9 @@
<ClInclude Include="ResourceString.h">
<DependentUpon>ResourceString.idl</DependentUpon>
</ClInclude>
<ClInclude Include="FontIconGlyphClassifier.h">
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="init.cpp" />
@@ -178,6 +181,9 @@
<DependentUpon>ResourceString.idl</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="FontIconGlyphClassifier.cpp">
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Midl Include="Converters.idl" />
@@ -185,6 +191,7 @@
<Midl Include="RunHistory.idl" />
<Midl Include="IDirectKeyListener.idl" />
<Midl Include="ResourceString.idl" />
<Midl Include="FontIconGlyphClassifier.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -2,10 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
@@ -13,34 +12,22 @@ public class MockSettingsInterface : ISettingsInterface
{
private readonly List<HistoryItem> _historyItems;
public event EventHandler HistoryChanged;
public bool GlobalIfURI { get; set; }
public string ShowHistory { get; set; }
public int HistoryItemCount { get; set; }
public MockSettingsInterface(string showHistory = "none", bool globalIfUri = true, List<HistoryItem> mockHistory = null)
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
{
_historyItems = mockHistory ?? new List<HistoryItem>();
GlobalIfURI = globalIfUri;
ShowHistory = showHistory;
HistoryItemCount = historyItemCount;
}
public List<ListItem> LoadHistory()
{
var listItems = new List<ListItem>();
foreach (var historyItem in _historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
});
}
listItems.Reverse();
return listItems;
}
public void SaveHistory(HistoryItem historyItem)
public void AddHistoryItem(HistoryItem historyItem)
{
if (historyItem is null)
{
@@ -50,19 +37,22 @@ public class MockSettingsInterface : ISettingsInterface
_historyItems.Add(historyItem);
// Simulate the same logic as SettingsManager
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
if (HistoryItemCount > 0)
{
while (_historyItems.Count > maxHistoryItems)
while (_historyItems.Count > HistoryItemCount)
{
_historyItems.RemoveAt(0); // Remove the oldest item
_historyItems.RemoveAt(0);
}
}
HistoryChanged?.Invoke(this, EventArgs.Empty);
}
// Helper method for testing
public void ClearHistory()
{
_historyItems.Clear();
HistoryChanged?.Invoke(this, EventArgs.Empty);
}
// Helper method for testing

View File

@@ -45,7 +45,7 @@ public class QueryTests : CommandPaletteUnitTestBase
}
[TestMethod]
public async Task LoadHistoryReturnsExpectedItems()
public async Task HistoryReturnsExpectedItems()
{
// Setup
var mockHistoryItems = new List<HistoryItem>
@@ -54,7 +54,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var page = new WebSearchListPage(settings);
@@ -77,7 +77,7 @@ public class QueryTests : CommandPaletteUnitTestBase
}
[TestMethod]
public async Task LoadHistoryMoreThanLimitation()
public async Task HistoryExceedingLimitReturnsMaxItems()
{
// Setup
var mockHistoryItems = new List<HistoryItem>
@@ -89,7 +89,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var page = new WebSearchListPage(settings);
@@ -109,7 +109,7 @@ public class QueryTests : CommandPaletteUnitTestBase
}
[TestMethod]
public async Task LoadHistoryWithDisableSetting()
public async Task HistoryWhenSetToNoneReturnEmptyList()
{
// Setup
var mockHistoryItems = new List<HistoryItem>
@@ -122,7 +122,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "None");
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var page = new WebSearchListPage(settings);

View File

@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
[TestClass]
public class SettingsManagerTests : CommandPaletteUnitTestBase
{
[TestMethod]
public async Task HistoryChangedEventIsRaisedWhenItemIsAdded()
{
// Setup
var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings);
var eventRaised = false;
try
{
settings.HistoryChanged += Handler;
// Act
settings.AddHistoryItem(new HistoryItem("test event", DateTime.UtcNow));
await Task.Delay(50);
// Assert
Assert.IsTrue(eventRaised, "Expected HistoryChanged to be raised when saving history.");
}
finally
{
settings.HistoryChanged -= Handler;
page.Dispose();
}
return;
void Handler(object s, EventArgs e) => eventRaised = true;
}
}

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>4</VersionMinor>
<VersionMinor>5</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -3,9 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
@@ -45,6 +43,28 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
}
public static int TopLevelResultLimit
{
get
{
var limitSetting = AllAppsSettings.Instance.SearchResultLimit;
if (limitSetting is null)
{
return -1;
}
var quantity = -1;
if (int.TryParse(limitSetting, out var result))
{
quantity = result;
}
return quantity;
}
}
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)

View File

@@ -20,6 +20,16 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _searchResultLimitChoices =
[
new ChoiceSetSetting.Choice(Resources.limit_none, "-1"),
new ChoiceSetSetting.Choice(Resources.limit_0, "0"),
new ChoiceSetSetting.Choice(Resources.limit_1, "1"),
new ChoiceSetSetting.Choice(Resources.limit_5, "5"),
new ChoiceSetSetting.Choice(Resources.limit_10, "10"),
new ChoiceSetSetting.Choice(Resources.limit_20, "20"),
];
#pragma warning disable SA1401 // Fields should be private
internal static AllAppsSettings Instance = new();
#pragma warning restore SA1401 // Fields should be private
@@ -42,6 +52,14 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value;
private readonly ChoiceSetSetting _searchResultLimitSource = new(
Namespaced(nameof(SearchResultLimit)),
Resources.limit_fallback_results_source,
Resources.limit_fallback_results_source_description,
_searchResultLimitChoices);
public string SearchResultLimit => _searchResultLimitSource.Value ?? string.Empty;
private readonly ToggleSetting _enableStartMenuSource = new(
Namespaced(nameof(EnableStartMenuSource)),
Resources.enable_start_menu_source,
@@ -87,6 +105,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
Settings.Add(_enableDesktopSource);
Settings.Add(_enableRegistrySource);
Settings.Add(_enablePathEnvironmentVariableSource);
Settings.Add(_searchResultLimitSource);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -133,17 +133,13 @@ internal sealed partial class AppListItem : ListItem
newCommands.Add(new Separator());
// 0x50 = P
// Full key chord would be Ctrl+P
var pinKeyChord = KeyChordHelpers.FromModifiers(true, false, false, false, 0x50, 0);
if (isPinned)
{
newCommands.Add(
new CommandContextItem(
new UnpinAppCommand(this.AppIdentifier))
{
RequestedShortcut = pinKeyChord,
RequestedShortcut = KeyChords.TogglePin,
});
}
else
@@ -152,7 +148,7 @@ internal sealed partial class AppListItem : ListItem
new CommandContextItem(
new PinAppCommand(this.AppIdentifier))
{
RequestedShortcut = pinKeyChord,
RequestedShortcut = KeyChords.TogglePin,
});
}

View File

@@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Management.Deployment;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UninstallApplicationCommand : InvokableCommand
{
// This is a ms-settings URI that opens the Apps & Features page in Windows Settings.
// It's correct and follows the Microsoft documentation:
// https://learn.microsoft.com/en-us/windows/apps/develop/launch/launch-settings-app#apps
private const string AppsFeaturesUri = "ms-settings:appsfeatures";
private readonly UWPApplication? _uwpTarget;
private readonly Win32Program? _win32Target;
public UninstallApplicationCommand(UWPApplication target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public UninstallApplicationCommand(Win32Program target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
}
private async Task<CommandResult> UninstallUwpAppAsync(UWPApplication app)
{
if (string.IsNullOrWhiteSpace(app.Package.FullName))
{
Logger.LogError($"Critical error while uninstalling: packageFullName cannot be null or empty.");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
try
{
// Which timeout to use for the uninstallation operation?
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
{
var packageManager = new PackageManager();
var result = await packageManager.RemovePackageAsync(app.Package.FullName).AsTask(cts.Token);
if (result.ErrorText is not null && result.ErrorText.Length > 0)
{
Logger.LogError($"Failed to uninstall {app.Package.FullName}: {result.ErrorText}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
}
// TODO: Update the Search results after uninstalling the app - unsure how to do this yet.
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_successful), app.DisplayName),
Result = CommandResult.GoHome(),
});
}
catch (OperationCanceledException)
{
Logger.LogError($"Timeout exceeded while uninstalling {app.Package.FullName}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
catch (UnauthorizedAccessException ex)
{
Logger.LogError($"Permission denied to uninstall {app.Package.FullName}. Elevated privileges may be required. Error: {ex.Message}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
catch (Exception ex)
{
Logger.LogError($"An unexpected error occurred during uninstallation of {app.Package.FullName}: {ex.Message}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
}
public override CommandResult Invoke()
{
if (_uwpTarget is not null)
{
return UninstallUwpAppAsync(_uwpTarget).ConfigureAwait(false).GetAwaiter().GetResult();
}
if (_win32Target is not null)
{
Process.Start(new ProcessStartInfo
{
FileName = AppsFeaturesUri,
UseShellExecute = true,
});
return CommandResult.Dismiss();
}
Logger.LogError("UninstallApplicationCommand invoked with no target.");
return CommandResult.Dismiss();
}
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Text;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UninstallApplicationConfirmation : InvokableCommand
{
private readonly UWPApplication? _uwpTarget;
private readonly Win32Program? _win32Target;
public UninstallApplicationConfirmation(UWPApplication target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public UninstallApplicationConfirmation(Win32Program target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
}
public override CommandResult Invoke()
{
UninstallApplicationCommand uninstallCommand;
var applicationTitle = Resources.uninstall_application;
if (_uwpTarget is not null)
{
uninstallCommand = new UninstallApplicationCommand(_uwpTarget);
applicationTitle = _uwpTarget.DisplayName;
}
else if (_win32Target is not null)
{
uninstallCommand = new UninstallApplicationCommand(_win32Target);
applicationTitle = _win32Target.Name;
}
else
{
Logger.LogError("UninstallApplicationCommand invoked with no target.");
return CommandResult.Dismiss();
}
var confirmArgs = new ConfirmationArgs()
{
Title = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_confirm_title), applicationTitle),
Description = Resources.uninstall_application_confirm_description,
PrimaryCommand = uninstallCommand,
IsPrimaryCommandCritical = true,
};
return CommandResult.Confirm(confirmArgs);
}
}

View File

@@ -21,4 +21,6 @@ internal sealed class Icons
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Ext.Apps;
internal static class KeyChords
{
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
internal static KeyChord RunAsAdministrator { get; } = WellKnownKeyChords.RunAsAdministrator;
internal static KeyChord RunAsDifferentUser { get; } = WellKnownKeyChords.RunAsDifferentUser;
internal static KeyChord TogglePin { get; } = WellKnownKeyChords.TogglePin;
}

View File

@@ -25,6 +25,7 @@
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>

View File

@@ -87,7 +87,7 @@ public class UWPApplication : IUWPApplication
new CommandContextItem(
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter),
RequestedShortcut = KeyChords.RunAsAdministrator,
});
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
@@ -97,7 +97,7 @@ public class UWPApplication : IUWPApplication
new CommandContextItem(
new CopyTextCommand(Location) { Name = Resources.copy_path })
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
RequestedShortcut = KeyChords.CopyFilePath,
});
commands.Add(
@@ -107,16 +107,24 @@ public class UWPApplication : IUWPApplication
Name = Resources.open_containing_folder,
})
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E),
RequestedShortcut = KeyChords.OpenFileLocation,
});
commands.Add(
new CommandContextItem(
new OpenInConsoleCommand(Package.Location))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
RequestedShortcut = KeyChords.OpenInConsole,
});
commands.Add(
new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
return commands;
}

View File

@@ -191,34 +191,44 @@ public class Win32Program : IProgram
commands.Add(new CommandContextItem(
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter),
RequestedShortcut = KeyChords.RunAsAdministrator,
});
commands.Add(new CommandContextItem(
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U),
RequestedShortcut = KeyChords.RunAsDifferentUser,
});
}
commands.Add(new CommandContextItem(
new CopyTextCommand(FullPath) { Name = Resources.copy_path })
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
RequestedShortcut = KeyChords.CopyFilePath,
});
commands.Add(new CommandContextItem(
new OpenPathCommand(ParentDirectory))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E),
RequestedShortcut = KeyChords.OpenFileLocation,
});
commands.Add(new CommandContextItem(
new OpenInConsoleCommand(ParentDirectory))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
RequestedShortcut = KeyChords.OpenInConsole,
});
if (AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication || AppType == ApplicationType.Win32Application)
{
commands.Add(new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
}
return commands;
}

View File

@@ -159,6 +159,78 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to 0.
/// </summary>
internal static string limit_0 {
get {
return ResourceManager.GetString("limit_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1.
/// </summary>
internal static string limit_1 {
get {
return ResourceManager.GetString("limit_1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 10.
/// </summary>
internal static string limit_10 {
get {
return ResourceManager.GetString("limit_10", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 20.
/// </summary>
internal static string limit_20 {
get {
return ResourceManager.GetString("limit_20", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 5.
/// </summary>
internal static string limit_5 {
get {
return ResourceManager.GetString("limit_5", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit the number of applications returned from the top level.
/// </summary>
internal static string limit_fallback_results_source {
get {
return ResourceManager.GetString("limit_fallback_results_source", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit fallback results to n apps.
/// </summary>
internal static string limit_fallback_results_source_description {
get {
return ResourceManager.GetString("limit_fallback_results_source_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unlimited.
/// </summary>
internal static string limit_none {
get {
return ResourceManager.GetString("limit_none", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open containing folder.
/// </summary>
@@ -258,6 +330,51 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall.
/// </summary>
internal static string uninstall_application {
get {
return ResourceManager.GetString("uninstall_application", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This app and its related information will be removed..
/// </summary>
internal static string uninstall_application_confirm_description {
get {
return ResourceManager.GetString("uninstall_application_confirm_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall &quot;{0}&quot;?.
/// </summary>
internal static string uninstall_application_confirm_title {
get {
return ResourceManager.GetString("uninstall_application_confirm_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error while uninstalling &apos;{0}&apos;.
/// </summary>
internal static string uninstall_application_failed {
get {
return ResourceManager.GetString("uninstall_application_failed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;{0}&apos; has been successfully uninstalled..
/// </summary>
internal static string uninstall_application_successful {
get {
return ResourceManager.GetString("uninstall_application_successful", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unpin.
/// </summary>

View File

@@ -198,4 +198,43 @@
<data name="unpin_app" xml:space="preserve">
<value>Unpin</value>
</data>
</root>
<data name="uninstall_application" xml:space="preserve">
<value>Uninstall</value>
</data>
<data name="uninstall_application_successful" xml:space="preserve">
<value>'{0}' has been successfully uninstalled.</value>
</data>
<data name="uninstall_application_failed" xml:space="preserve">
<value>Error while uninstalling '{0}'</value>
</data>
<data name="uninstall_application_confirm_description" xml:space="preserve">
<value>This app and its related information will be removed.</value>
</data>
<data name="uninstall_application_confirm_title" xml:space="preserve">
<value>Uninstall "{0}"?</value>
</data>
<data name="limit_1" xml:space="preserve">
<value>1</value>
</data>
<data name="limit_5" xml:space="preserve">
<value>5</value>
</data>
<data name="limit_10" xml:space="preserve">
<value>10</value>
</data>
<data name="limit_20" xml:space="preserve">
<value>20</value>
</data>
<data name="limit_fallback_results_source" xml:space="preserve">
<value>Limit the number of applications returned from the top level</value>
</data>
<data name="limit_fallback_results_source_description" xml:space="preserve">
<value>Limit fallback results to n apps</value>
</data>
<data name="limit_0" xml:space="preserve">
<value>0</value>
</data>
<data name="limit_none" xml:space="preserve">
<value>Unlimited</value>
</data>
</root>

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -11,19 +12,25 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
public partial class ClipboardHistoryCommandsProvider : CommandProvider
{
private readonly ListItem _clipboardHistoryListItem;
private readonly SettingsManager _settingsManager = new();
public ClipboardHistoryCommandsProvider()
{
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
{
Title = Properties.Resources.list_item_title,
Subtitle = Properties.Resources.list_item_subtitle,
Icon = Icons.ClipboardListIcon,
MoreCommands = [
new CommandContextItem(_settingsManager.Settings.SettingsPage),
],
};
DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon;
Id = "Windows.ClipboardHistory";
Settings = _settingsManager.Settings;
}
public override IListItem[] TopLevelCommands()

View File

@@ -0,0 +1,31 @@
// 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.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
internal sealed partial class DeleteItemCommand : InvokableCommand
{
private readonly ClipboardItem _clipboardItem;
internal DeleteItemCommand(ClipboardItem clipboardItem)
{
_clipboardItem = clipboardItem;
Name = Properties.Resources.delete_command_name;
Icon = Icons.DeleteIcon;
}
public override CommandResult Invoke()
{
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
return CommandResult.ShowToast(new ToastArgs
{
Message = Properties.Resources.delete_toast_text,
Result = CommandResult.KeepOpen(),
});
}
}

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
@@ -14,11 +15,13 @@ internal sealed partial class PasteCommand : InvokableCommand
{
private readonly ClipboardItem _clipboardItem;
private readonly ClipboardFormat _clipboardFormat;
private readonly ISettingOptions _settings;
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat, ISettingOptions settings)
{
_clipboardItem = clipboardItem;
_clipboardFormat = clipboardFormat;
_settings = settings;
Name = Properties.Resources.paste_command_name;
Icon = Icons.PasteIcon;
}
@@ -39,7 +42,11 @@ internal sealed partial class PasteCommand : InvokableCommand
ClipboardHelper.SendPasteKeyCombination();
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
if (!_settings.KeepAfterPaste)
{
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
}
return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
}
}

View File

@@ -0,0 +1,12 @@
// 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.
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
public interface ISettingOptions
{
bool KeepAfterPaste { get; }
bool DeleteFromHistoryRequiresConfirmation { get; }
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using Microsoft.CmdPal.Ext.ClipboardHistory.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
{
private const string Namespace = "clipboardHistory";
private static string Namespaced(string propertyName) => $"{Namespace}.{propertyName}";
private readonly ToggleSetting _keepAfterPaste = new(
Namespaced(nameof(KeepAfterPaste)),
Resources.settings_keep_after_paste_title!,
Resources.settings_keep_after_paste_description!,
false);
private readonly ToggleSetting _confirmDelete = new(
Namespaced(nameof(DeleteFromHistoryRequiresConfirmation)),
Resources.settings_confirm_delete_title!,
Resources.settings_confirm_delete_description!,
true);
public bool KeepAfterPaste => _keepAfterPaste.Value;
public bool DeleteFromHistoryRequiresConfirmation => _confirmDelete.Value;
private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_keepAfterPaste);
Settings.Add(_confirmDelete);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (_, _) => SaveSettings();
}
}

View File

@@ -14,5 +14,7 @@ internal sealed class Icons
internal static IconInfo PasteIcon { get; } = new("\uE77F");
internal static IconInfo DeleteIcon { get; } = new("\uE74D");
internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg");
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
internal static class KeyChords
{
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
}

View File

@@ -7,7 +7,9 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
@@ -16,9 +18,11 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
public class ClipboardItem
{
public string? Content { get; set; }
public string? Content { get; init; }
public required ClipboardHistoryItem Item { get; set; }
public required ClipboardHistoryItem Item { get; init; }
public required ISettingOptions Settings { get; init; }
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
@@ -87,6 +91,19 @@ public class ClipboardItem
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
});
var deleteConfirmationCommand = new ConfirmableCommand()
{
Command = new DeleteItemCommand(this),
ConfirmationTitle = Properties.Resources.delete_confirmation_title!,
ConfirmationMessage = Properties.Resources.delete_confirmation_message!,
IsConfirmationRequired = () => Settings.DeleteFromHistoryRequiresConfirmation,
};
var deleteContextMenuItem = new CommandContextItem(deleteConfirmationCommand)
{
IsCritical = true,
RequestedShortcut = KeyChords.DeleteEntry,
};
if (IsImage)
{
var iconData = new IconData(ImageData);
@@ -103,7 +120,9 @@ public class ClipboardItem
Metadata = metadata.ToArray(),
},
MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image))
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image, Settings)),
new Separator(),
deleteContextMenuItem,
],
};
}
@@ -126,8 +145,10 @@ public class ClipboardItem
Metadata = metadata.ToArray(),
},
MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)),
],
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text, Settings)),
new Separator(),
deleteContextMenuItem,
],
};
}
else

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,11 +18,15 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
internal sealed partial class ClipboardHistoryListPage : ListPage
{
private readonly SettingsManager _settingsManager;
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
private readonly string _defaultIconPath;
public ClipboardHistoryListPage()
public ClipboardHistoryListPage(SettingsManager settingsManager)
{
ArgumentNullException.ThrowIfNull(settingsManager);
_settingsManager = settingsManager;
clipboardHistory = [];
_defaultIconPath = string.Empty;
Icon = Icons.ClipboardListIcon;
@@ -84,11 +89,11 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
if (item.Content.Contains(StandardDataFormats.Text))
{
var text = await item.Content.GetTextAsync();
items.Add(new ClipboardItem { Content = text, Item = item });
items.Add(new ClipboardItem { Settings = _settingsManager, Content = text, Item = item });
}
else if (item.Content.Contains(StandardDataFormats.Bitmap))
{
items.Add(new ClipboardItem { Item = item });
items.Add(new ClipboardItem { Settings = _settingsManager, Item = item });
}
}

View File

@@ -96,6 +96,42 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
public static string delete_command_name {
get {
return ResourceManager.GetString("delete_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure you want to delete this item from clipboard history? This action cannot be undone..
/// </summary>
public static string delete_confirmation_message {
get {
return ResourceManager.GetString("delete_confirmation_message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete item?.
/// </summary>
public static string delete_confirmation_title {
get {
return ResourceManager.GetString("delete_confirmation_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Deleted from clipboard history.
/// </summary>
public static string delete_toast_text {
get {
return ResourceManager.GetString("delete_toast_text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
/// </summary>
@@ -140,5 +176,41 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
return ResourceManager.GetString("provider_display_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string settings_confirm_delete_description {
get {
return ResourceManager.GetString("settings_confirm_delete_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show a confirmation dialog when manually deleting an item.
/// </summary>
public static string settings_confirm_delete_title {
get {
return ResourceManager.GetString("settings_confirm_delete_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string settings_keep_after_paste_description {
get {
return ResourceManager.GetString("settings_keep_after_paste_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keep items in clipboard history after pasting.
/// </summary>
public static string settings_keep_after_paste_title {
get {
return ResourceManager.GetString("settings_keep_after_paste_title", resourceCulture);
}
}
}
}

View File

@@ -144,4 +144,28 @@
<data name="clipboard_failed_to_load" xml:space="preserve">
<value>Loading clipboard history failed</value>
</data>
<data name="delete_command_name" xml:space="preserve">
<value>Delete</value>
</data>
<data name="delete_toast_text" xml:space="preserve">
<value>Deleted from clipboard history</value>
</data>
<data name="settings_keep_after_paste_description" xml:space="preserve">
<value />
</data>
<data name="settings_keep_after_paste_title" xml:space="preserve">
<value>Keep items in clipboard history after pasting</value>
</data>
<data name="settings_confirm_delete_title" xml:space="preserve">
<value>Show a confirmation dialog when manually deleting an item</value>
</data>
<data name="settings_confirm_delete_description" xml:space="preserve">
<value />
</data>
<data name="delete_confirmation_title" xml:space="preserve">
<value>Delete item?</value>
</data>
<data name="delete_confirmation_message" xml:space="preserve">
<value>Are you sure you want to delete this item from clipboard history? This action cannot be undone.</value>
</data>
</root>

View File

@@ -91,9 +91,9 @@ internal sealed partial class IndexerListItem : ListItem
}
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }));
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }));
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)));
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }) { RequestedShortcut = KeyChords.OpenFileLocation });
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }) { RequestedShortcut = KeyChords.CopyFilePath });
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)) { RequestedShortcut = KeyChords.OpenInConsole });
commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath)));
if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Ext.Indexer;
internal static class KeyChords
{
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
}

View File

@@ -17,6 +17,7 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
{
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.LockIcon;
var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi;
var hideEmptyRB = settings.HideEmptyRecycleBin();

View File

@@ -22,6 +22,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
{
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.TimeDateIcon;
_settingsManager = settings;
_timestamp = timestamp;

View File

@@ -79,16 +79,22 @@ public sealed partial class TimeDateCalculator
}
else
{
List<(int Score, AvailableResult Item)> itemScores = [];
// Generate filtered list of results
foreach (var f in availableFormats)
{
var score = f.Score(query, f.Label, f.AlternativeSearchTag);
if (score > 0)
{
results.Add(f.ToListItem());
itemScores.Add((score, f));
}
}
results = itemScores
.OrderByDescending(s => s.Score)
.Select(s => s.Item.ToListItem())
.ToList();
}
if (results.Count == 0)

View File

@@ -34,9 +34,9 @@ internal sealed partial class SearchWebCommand : InvokableCommand
return CommandResult.KeepOpen();
}
if (_settingsManager.ShowHistory != Resources.history_none)
if (_settingsManager.HistoryItemCount != 0)
{
_settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now));
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
}
return CommandResult.Dismiss();

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
internal sealed class HistoryStore
{
private readonly string _filePath;
private readonly List<HistoryItem> _items = [];
private readonly Lock _lock = new();
private int _capacity;
public event EventHandler? Changed;
public HistoryStore(string filePath, int capacity)
{
ArgumentNullException.ThrowIfNull(filePath);
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
_filePath = filePath;
_capacity = capacity;
_items.AddRange(LoadFromDiskSafe());
TrimNoLock();
}
public IReadOnlyList<HistoryItem> HistoryItems
{
get
{
lock (_lock)
{
return [.. _items];
}
}
}
public void Add(HistoryItem item)
{
ArgumentNullException.ThrowIfNull(item);
lock (_lock)
{
_items.Add(item);
_ = TrimNoLock();
SaveNoLock();
}
Changed?.Invoke(this, EventArgs.Empty);
}
public void SetCapacity(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
bool trimmed;
lock (_lock)
{
_capacity = capacity;
trimmed = TrimNoLock();
if (trimmed)
{
SaveNoLock();
}
}
if (trimmed)
{
Changed?.Invoke(this, EventArgs.Empty);
}
}
private bool TrimNoLock()
{
var max = _capacity;
if (_items.Count > max)
{
_items.RemoveRange(0, _items.Count - max);
return true;
}
return false;
}
private List<HistoryItem> LoadFromDiskSafe()
{
try
{
if (!File.Exists(_filePath))
{
return [];
}
var fileContent = File.ReadAllText(_filePath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
return historyItems;
}
catch (Exception ex)
{
Logger.LogError("Unable to load history", ex);
return [];
}
}
private void SaveNoLock()
{
var json = JsonSerializer.Serialize(_items, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_filePath, json);
}
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -9,11 +10,13 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public interface ISettingsInterface
{
event EventHandler? HistoryChanged;
public bool GlobalIfURI { get; }
public string ShowHistory { get; }
public int HistoryItemCount { get; }
public List<ListItem> LoadHistory();
public IReadOnlyList<HistoryItem> HistoryItems { get; }
public void SaveHistory(HistoryItem historyItem);
public void AddHistoryItem(HistoryItem historyItem);
}

View File

@@ -4,11 +4,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using ManagedCommon;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -16,19 +13,26 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
private readonly string _historyPath;
private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
private static readonly string _namespace = "websearch";
public event EventHandler? HistoryChanged
{
add => _history.Changed += value;
remove => _history.Changed -= value;
}
private readonly HistoryStore _history;
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _choices =
[
new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none),
new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1),
new ChoiceSetSetting.Choice(Resources.history_5, Resources.history_5),
new ChoiceSetSetting.Choice(Resources.history_10, Resources.history_10),
new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20),
new ChoiceSetSetting.Choice(Resources.history_none, "None"),
new ChoiceSetSetting.Choice(Resources.history_1, "1"),
new ChoiceSetSetting.Choice(Resources.history_5, "5"),
new ChoiceSetSetting.Choice(Resources.history_10, "10"),
new ChoiceSetSetting.Choice(Resources.history_20, "20"),
];
private readonly ToggleSetting _globalIfURI = new(
@@ -37,17 +41,34 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.plugin_global_if_uri,
false);
private readonly ChoiceSetSetting _showHistory = new(
Namespaced(nameof(ShowHistory)),
Resources.plugin_show_history,
Resources.plugin_show_history,
private readonly ChoiceSetSetting _historyItemCount = new(
Namespaced(HistoryItemCountLegacySettingsKey),
Resources.plugin_history_item_count,
Resources.plugin_history_item_count,
_choices);
public bool GlobalIfURI => _globalIfURI.Value;
public string ShowHistory => _showHistory.Value ?? string.Empty;
public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
internal static string SettingsJsonPath()
public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount);
LoadSettings();
// Initialize history store after loading settings to get the correct capacity
_history = new HistoryStore(HistoryStateJsonPath(), HistoryItemCount);
Settings.SettingsChanged += (_, _) => SaveSettings();
}
private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
@@ -56,7 +77,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
return Path.Combine(directory, "settings.json");
}
internal static string HistoryStateJsonPath()
private static string HistoryStateJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
@@ -65,156 +86,30 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
return Path.Combine(directory, "websearch_history.json");
}
public void SaveHistory(HistoryItem historyItem)
public void AddHistoryItem(HistoryItem historyItem)
{
if (historyItem is null)
{
return;
}
try
{
List<HistoryItem> historyItems;
// Check if the file exists and load existing history
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
}
else
{
historyItems = [];
}
// Add the new history item
historyItems.Add(historyItem);
// Determine the maximum number of items to keep based on ShowHistory
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
// Keep only the most recent `maxHistoryItems` items
while (historyItems.Count > maxHistoryItems)
{
historyItems.RemoveAt(0); // Remove the oldest item
}
}
// Serialize the updated list back to JSON and save it
var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, historyJson);
_history.Add(historyItem);
}
catch (Exception ex)
{
Logger.LogError("Failed to add item to the search history", ex);
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
}
}
public List<ListItem> LoadHistory()
{
try
{
if (!File.Exists(_historyPath))
{
return [];
}
// Read and deserialize JSON into a list of HistoryItem objects
var fileContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Convert each HistoryItem to a ListItem
var listItems = new List<ListItem>();
foreach (var historyItem in historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture), // Ensures consistent formatting
});
}
listItems.Reverse();
return listItems;
}
catch (Exception ex)
{
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
return [];
}
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
_historyPath = HistoryStateJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_showHistory);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
private void ClearHistory()
{
try
{
if (File.Exists(_historyPath))
{
// Delete the history file
File.Delete(_historyPath);
// Log that the history was successfully cleared
ExtensionHost.LogMessage(new LogMessage() { Message = "History cleared successfully." });
}
else
{
// Log that there was no history file to delete
ExtensionHost.LogMessage(new LogMessage() { Message = "No history file found to clear." });
}
}
catch (Exception ex)
{
// Log any exception that occurs
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to clear history: {ex}" });
}
}
public override void SaveSettings()
{
base.SaveSettings();
try
{
if (ShowHistory == Resources.history_none)
{
ClearHistory();
}
else if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
// Trim the history file if there are more items than the new limit
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > maxHistoryItems)
{
// Trim the list to keep only the most recent `maxHistoryItems` items
historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList();
// Save the trimmed history back to the file
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, trimmedHistoryJson);
}
}
}
_history.SetCapacity(HistoryItemCount);
}
catch (Exception ex)
{
Logger.LogError("Failed to save the search history", ex);
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
}
}

View File

@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
@@ -16,31 +16,30 @@ using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{
private readonly string _iconPath = string.Empty;
private readonly List<ListItem>? _historyItems;
private readonly IconInfo _newSearchIcon = new(string.Empty);
private readonly ISettingsInterface _settingsManager;
private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private List<ListItem> _allItems;
private IListItem[] _allItems = [];
private List<ListItem> _historyItems = [];
public WebSearchListPage(ISettingsInterface settingsManager)
{
ArgumentNullException.ThrowIfNull(settingsManager);
Name = Resources.command_item_title;
Title = Resources.command_item_title;
Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
_allItems = [];
Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager;
_historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null;
if (_historyItems is not null)
{
_allItems.AddRange(_historyItems);
}
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
// It just looks viewer to have string twice on the page, and default placeholder is good enough
PlaceholderText = _allItems.Count > 0 ? Resources.plugin_description : string.Empty;
PlaceholderText = _allItems.Length > 0 ? Resources.plugin_description : string.Empty;
EmptyContent = new CommandItem(new NoOpCommand())
{
@@ -48,45 +47,102 @@ internal sealed partial class WebSearchListPage : DynamicListPage
Title = Properties.Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
};
UpdateHistory();
RequeryAndUpdateItems(SearchText);
}
public List<ListItem> Query(string query)
private void SettingsManagerOnHistoryChanged(object? sender, EventArgs e)
{
ArgumentNullException.ThrowIfNull(query);
IEnumerable<ListItem>? filteredHistoryItems = null;
UpdateHistory();
RequeryAndUpdateItems(SearchText);
}
if (_historyItems is not null)
private void UpdateHistory()
{
List<ListItem> history = [];
if (_settingsManager.HistoryItemCount > 0)
{
filteredHistoryItems = _settingsManager.ShowHistory != Resources.history_none ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null;
var items = _settingsManager.HistoryItems;
for (var index = items.Count - 1; index >= 0; index--)
{
var historyItem = items[index];
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture),
});
}
}
var results = new List<ListItem>();
lock (_sync)
{
_historyItems = history;
}
}
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IconInfo newSearchIcon)
{
ArgumentNullException.ThrowIfNull(query);
var filteredHistoryItems = settingsManager.HistoryItemCount > 0
? ListHelpers.FilterList(historySnapshot, query)
: [];
var results = new List<IListItem>();
if (!string.IsNullOrEmpty(query))
{
var searchTerm = query;
var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager))
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
{
Title = searchTerm,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
Icon = new IconInfo(_iconPath),
Icon = newSearchIcon,
};
results.Add(result);
}
if (filteredHistoryItems is not null)
results.AddRange(filteredHistoryItems);
return [.. results];
}
private void RequeryAndUpdateItems(string search)
{
List<ListItem> historySnapshot;
lock (_sync)
{
results.AddRange(filteredHistoryItems);
historySnapshot = _historyItems;
}
return results;
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _newSearchIcon);
lock (_sync)
{
_allItems = items;
}
RaiseItemsChanged();
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_allItems = [.. Query(newSearch)];
RaiseItemsChanged(0);
RequeryAndUpdateItems(newSearch);
}
public override IListItem[] GetItems() => [.. _allItems];
public override IListItem[] GetItems()
{
lock (_sync)
{
return _allItems;
}
}
public void Dispose()
{
_settingsManager.HistoryChanged -= SettingsManagerOnHistoryChanged;
GC.SuppressFinalize(this);
}
}

View File

@@ -168,6 +168,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Determines the number of history items to show from previous searches.
/// </summary>
public static string plugin_history_item_count {
get {
return ResourceManager.GetString("plugin_history_item_count", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to In the default browser.
/// </summary>
@@ -231,15 +240,6 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Determines the number of history items to show from previous searches.
/// </summary>
public static string plugin_show_history {
get {
return ResourceManager.GetString("plugin_show_history", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>

View File

@@ -172,7 +172,7 @@
<data name="plugin_search_failed" xml:space="preserve">
<value>Failed to open {0}.</value>
</data>
<data name="plugin_show_history" xml:space="preserve">
<data name="plugin_history_item_count" xml:space="preserve">
<value>Determines the number of history items to show from previous searches</value>
</data>
<data name="settings_page_name" xml:space="preserve">

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
@@ -15,6 +16,9 @@ public partial class WebSearchCommandsProvider : CommandProvider
private readonly SettingsManager _settingsManager = new();
private readonly FallbackExecuteSearchItem _fallbackItem;
private readonly FallbackOpenURLItem _openUrlFallbackItem;
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands;
public WebSearchCommandsProvider()
{
@@ -25,18 +29,27 @@ public partial class WebSearchCommandsProvider : CommandProvider
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
}
public override ICommandItem[] TopLevelCommands()
{
return [new WebSearchTopLevelCommandItem(_settingsManager)
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
{
MoreCommands = [
MoreCommands =
[
new CommandContextItem(Settings!.SettingsPage),
],
}
];
};
_topLevelItems = [_webSearchTopLevelItem];
_fallbackCommands = [_openUrlFallbackItem, _fallbackItem];
}
public override IFallbackCommandItem[]? FallbackCommands() => [_openUrlFallbackItem, _fallbackItem];
public override ICommandItem[] TopLevelCommands() => _topLevelItems;
public override IFallbackCommandItem[]? FallbackCommands() => _fallbackCommands;
public override void Dispose()
{
_webSearchTopLevelItem?.Dispose();
base.Dispose();
GC.SuppressFinalize(this);
}
}

Some files were not shown because too many files have changed in this diff Show More