Compare commits

...

32 Commits

Author SHA1 Message Date
Dustin L. Howett
200fd3437d Tests: do not use *relative drive letter*
Since we switched to running the build on C:\, this test started failing. C: means "current directory on the C drive"!
2025-10-30 15:33:00 -05:00
Jaylyn Barbee
1783812f1f Light Switch is not clearing manual override if the boundary happens while offline (#43072)
Bug: If light switch is transitioning while the computer is offline,
Light Switch will not clear the manual override at the boundary.
Fix: Ensure the "Clear override" logic checks if we are at or past a
boundary point.
2025-10-29 09:23:43 -07:00
Shawn Yuan
94b5bc62de Fix title bar overlapping issue (#43022)
<!-- 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 introduces a small UI improvement to the
`ShellPage.xaml` file. The change sets a breakpoint for the compact
state of the title bar, which will help the interface adapt better to
different window sizes.

* UI responsiveness: Added `CompactStateBreakpoint="900"` to the
`AppTitleBar` control in `ShellPage.xaml` to improve layout adaptation
for smaller window widths.

**Before:**


https://github.com/user-attachments/assets/5ec424f4-b441-4eff-967f-a97cc92e180a


**After:**


https://github.com/user-attachments/assets/a1487c39-dce7-4055-90e9-aa6ed95e4beb



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

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-10-29 10:32:09 +08:00
Shawn Yuan
0d18727e81 [CmdPal] Added recenter window support. (#38943)
<!-- 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 a new setting option to Command Palette that allows users to
choose whether the window should be recentered on every launch or
remember its last position. This enhancement improves user experience by
maintaining window positioning preferences across sessions.

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

- [x] **Closes:** #38310
- [x] **Communication:** I've discussed this with core contributors
already. If 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

This PR adds a "Recenter window on launch" setting to Command Palette
with the following changes:

1. Added a new `RecenterWindow` property to `SettingsModel` (default is
true for backward compatibility)
2. Added corresponding property to `SettingsViewModel` for binding
3. Created a new `WindowPosition` class to track window position and
size
4. Modified `MainWindow.xaml.cs` to:
    - Track window position and size changes
    - Update position memory when window is modified or dismissed
    - Respect the recenter setting when showing the window
5. Added UI controls in the settings page with proper localization
strings

![image](https://github.com/user-attachments/assets/1c1db9f6-b23e-47c0-9a03-93906d4c9a42)



This feature allows users who prefer to have the Command Palette appear
in a specific screen location to maintain that preference, while others
can continue using the centered window behavior.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
1. Manually verified that with the setting enabled (default), the window
centers on launch
2. Verified that with the setting disabled, the window appears at its
last position

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-10-28 14:37:45 -05:00
Jiří Polášek
1e40d6b15b CmdPal: Extension list search (#41453)
## Summary of the Pull Request

Adds a rough implementation that appears to work, but it hasn’t been
tested. Don’t rely on it yet—I’m too tired to test properly or write a
polished commit message.

Pictures? Pictures!

<img width="2720" height="1624" alt="image"
src="https://github.com/user-attachments/assets/4f9c3360-8d08-4a34-888f-c6890afd0159"
/>

<img width="2647" height="1558" alt="image"
src="https://github.com/user-attachments/assets/3d6c23bc-f26d-4e31-ae09-d91efd304048"
/>

<img width="2683" height="1536" alt="image"
src="https://github.com/user-attachments/assets/6017b2d4-ccd9-4555-b81f-e2b1f78c48e5"
/>

<img width="577" height="334" alt="image"
src="https://github.com/user-attachments/assets/987fea99-05b2-4d31-b90c-327122daa241"
/>


https://github.com/user-attachments/assets/65a8f8ce-ae09-4272-881d-0b6f56cecfae

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

- [x] Closes: #41451
- [x] Closes: #42031
- [x] Closes: #41800
- [ ] **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: Niels Laute <niels.laute@live.nl>
Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
2025-10-28 14:36:42 -05:00
Guilherme
de00cbf20a [CmdPal] Fix filters visibility on non-ListPage (#42828)
<!-- 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 aims to fix the issue where filters from a ListPage remain
visible when navigating to other pages.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

![FiltersIssue](https://github.com/user-attachments/assets/b0ad6059-9a11-4e12-821d-7202358e25bb)

### After:

![FiltersFix](https://github.com/user-attachments/assets/b9ee71ee-cb5d-4ef9-b9fc-bc2e2a710b5c)

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

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2025-10-28 14:32:09 -05:00
Jiří Polášek
c4e96c7ee9 CmdPal: Add hints about bookmark placeholders to the Add/Edit Bookmark form (#42793)
## Summary of the Pull Request

This PR adds a short explanation to the Add/Edit Bookmark form,
describing how to use placeholders in bookmark URLs or paths

<img width="823" height="525" alt="image"
src="https://github.com/user-attachments/assets/b66adfc4-2dbc-4934-8796-9d4ad46d9f5f"
/>

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

- [x] Closes: #42265 
- [ ] **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-10-28 14:28:46 -05:00
Jiří Polášek
103429b4d7 CmdPal: Add hidden window as owner for tool windows (#42902)
## Summary of the Pull Request

This PR changes the method used to hide tool windows from the taskbar
and Alt+Tab to a more reliable approach.
Previously, this was achieved by adding `WS_EX_TOOLWINDOW` to an unowned
top-level window, which proved unreliable in several scenarios.

The new implementation assigns a hidden window as the owner of each tool
window.
This ensures that the window does not appear on the taskbar even when
the Windows setting
**Settings → System → Multitasking → On the taskbar, show all opened
windows** is set to **On all desktops**.

## Change log one-liner

Fixes Command Palette windows occasionally appearing on the taskbar
under certain system settings.

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

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

<!-- 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 alongside the stable CmdPal on a system with
2025-10-28 14:26:01 -05:00
Jaylyn Barbee
01fb831e4e [Light Switch] Light Switch should detect changes in Windows Settings and treat as manual override (same as using shortcut) (#42882)
<!-- 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 ensures that Light Switch detects changes to app/system theme
from Windows Settings. This PR also introduces new behavior where
switching the schedule will cause an instant update to the theme if
necessary.

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

- [x] Closes: #42878
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **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
2025-10-28 12:00:38 -07:00
Sam Rueby
d197af3da9 CmdPal's search bar now accepts page up/down keyboard strokes. (#41886)
<!-- 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 page up/down keys now function while the search box is focused.

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

- [ X ] Closes: #41877
- [ ] **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
Previously, the page up/down keys only performed any action while an
item in the list was focused. The page up/down keys did not have any
effect while the search box was focused, however the up/down arrows do
have effect. This PR enables the page up/down keys while the search box
is focused.

There is a caveat here. The page up/down behavior is not consistent. I
do not see a way to tell the ListView to perform its native page up/down
function. Instead, I manually calculate roughly which item to scroll-to.
Because of this, the amount of scroll between when the search box is
focused and when an item in the ListView is focused is not consistent.

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


![pageupdown](https://github.com/user-attachments/assets/b30f6e4e-03de-45bd-8570-0b06850bef24)
In this GIF:
1. CmdPal appears
2. SearchBar focused, down/up arrow keys.
3. SearchBar focused, page down/up keys.
4. Tab to item in ListView
5. ListView item focused down/up arrow keys.
6. ListView item focused page down/up keys.
7. SearchBar focused
8. Filter "abc"
9. SearchBar focused page down/up keys.
2025-10-28 12:56:17 -05:00
Jiří Polášek
a4791cc493 CmdPal: Ensure CommandItemViewModel reacts to changes of replaced Command (#42982)
## Summary of the Pull Request

This PR resolves the issue with CommandItemViewModel's subscription to
changes in the associated Command when it gets replaced by another
Command. The current implementation removes the handler from the old
command but fails to attach a new one.

## Change log one-liner


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

- [x] Closes: #42981 
- [ ] **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-10-28 11:22:09 -05:00
leileizhang
1a1894472a Fix package identity build issue (#43019)
<!-- 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
<img width="1988" height="360" alt="image"
src="https://github.com/user-attachments/assets/85d994a9-3799-4f16-a836-85ca264b873c"
/>

Add an EntryPoint to the AppxManifest to fix the issue.
<!-- 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-10-28 17:16:03 +08:00
Jiří Polášek
5d6f96559c Multiple toys: Exclude TitleBars from tab navigation (#42030)
## Summary of the Pull Request

This PR removes title bar controls from tab navigation, solving one of
hidden tab stops (the other being #40637).

Affected apps:
- Command Palette - Settings
- Environment Variables
- File Locksmith
- Hosts File Editor
- Registry Preview
- Settings (the search box in the title bar is still tab navigable)

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

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

All apps were manually tests, by pressing tab and shift + tab.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-10-28 09:40:20 +08:00
Kai Tao
b774e13176 Fix the foreground style for find my mouse (#42865)
<!-- 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

Find my mouse should use full transparent window

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

- [X] Closes: #42758
- [ ] **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/75c73eb3-04bb-438c-8823-3c9f18923cc6
2025-10-27 09:33:26 +08:00
Jiří Polášek
e256e79685 CmdPal: Fix search box text selection in ShellPage.GoHome (#42937)
## Summary of the Pull Request

This PR fixes an issue where `ShellPage.GoHome` wouldn’t select the
search box text when the current page was already the home page.

In that case, the navigation stack was empty, and no code was executed
because focusing the text had been delegated to the `GoBack` operation.

## Change log one-liner

Ensured search text is selected when Go home when activated and
Highlight search on activate are both enabled.

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

- [x] Closes: #42443
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [x] **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-10-25 19:51:53 -05:00
Jiří Polášek
623c804093 ManagedCommon: Log correct HRESULT for the inner exception (#42178)
## Summary of the Pull Request

This PR fixes incorrect HRESULT for inner exception when an error is
logged.

<!-- 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-10-25 19:50:07 -05:00
Michael Jolley
20188bda9b File search now has filters (#42141)
Closes #39260

Search for all files & folders, folders only, or files only.

Enjoy.


https://github.com/user-attachments/assets/43ba93f5-dfc5-4e73-8414-547cf99dcfcf
2025-10-24 19:16:21 -05:00
Jiří Polášek
6e5ad11bc3 CmdpPal: SearchBox visibility and async loading race (#42783)
## Summary of the Pull Request

This PR introduces two related fixes to improve the stability and
reliability of navigation and search UI behavior in the shell:

- **Ensure search box visibility is correctly updated**  
- `ShellViewModel` previously set `IsSearchBoxVisible` after navigation
to the page, but didn’t update it when the value changed. While the
value isn’t expected to change dynamically, the property initialization
is asynchronous, which could cause a race condition.
- As a defensive measure, this also changes the default value of
uninitialized property to make it visible by default.

- **Cancel asynchronous focus placement if navigation changes**  
- Ensures that any pending asynchronous focus operation is cancelled
when another navigation occurs before it completes.


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

- [x] Closes: #42782
- [ ] **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-10-24 19:15:34 -05:00
Jiří Polášek
0e36e7e7a7 CmdPal: Add keyboard shortcut (Ctrl+,) to open Settings (#42787)
## Summary of the Pull Request

This PR introduces a new keyboard shortcut `Ctrl + ,` that opens the
Settings window directly.


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

- [x] Closes: #42785 
- [ ] **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-10-24 19:12:59 -05:00
moooyo
82dc4cdc18 [CmdPal] Replace complex cancellation token mechanism with a simple task queue. (#42356)
<!-- 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
Just consider user are trying to search a long name such as "Visual
Studio Code"
The old mechanism:
User input: V
Task: V
Then input: i
Task cancel for V and start task i
etc...

The problem is:
1. I don't think we can really cancel the most time-cost part (Find
packages from WinGet).
2. User cannot see anything before they really end the input and the
last task complete. UX exp is so bad.
3. It's so complex to maintain. Hard to understand for the new
contributor.

New mechanism:
User input: V
Task: V
Then input: i 
Prev Task is still running but mark the next task is i
Input: s
Prev Task is still running but override the next task to s
etc...

We can get:
1. User can see some results if prev task complete.
2. It's simple to understand
3. The extra time cost I think will not too much. Because we ignored the
middle input.

Compare:


https://github.com/user-attachments/assets/f45f4073-efab-4f43-87f0-f47b727f36dc



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

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2025-10-24 14:28:30 -05:00
Kai Tao
c71fdca277 Hybrid CRT for powertys (#42073)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
<!-- Please review the items on the PR checklist before submitting-->
Hybrid CRT across powertoys for better bundle size

## 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
Bundle Size comparasion:

| bundle        | Before Hybrid CRT | After Hybrid CRT | diff |
|---------------|-------------------|------------------|------|
| x64-user      | 317M              | 310M             | 7M   |
| x64-machine   | 317M              |310M             | 7M   |
| arm64-user    | 305M              | 299M             | 6M   |
| arm64-machine | 305M              | 299M             | 6M   |


Did verification on a sandbox machine, every module launches as
expected, no dependency issue
2025-10-24 10:24:39 +08:00
Jiří Polášek
a69f7fa806 CmdPal: Update top-level item view model to reflect change of the associated command (#42806)
## Summary of the Pull Request

This PR implements a fix that ensures the top-level command's alias,
hotkey, and tags are automatically updated whenever the associated
command is modified, as the command defines the actual identity of the
item.

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

- [x] Closes: #42796
- [x] Related to: #42807
- [ ] **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-10-23 16:57:12 -05:00
Jiří Polášek
cd5f753140 CmdPal: Migrate bookmarks manually (#42814)
<!-- 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 the migration of bookmarks from versions prior to 0.95,
resolving an issue where hotkeys and aliases wouldn’t persist on
bookmarks created with Command Palette 0.94 or earlier.

It removes ID auto-fixing from `BookmarkData` in favor of an explicit
migration step handled by `BookmarkManager`.

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

- [x] Closes: #42796
- [ ] **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-10-23 16:53:06 -05:00
Clint Rutkas
c628b4901d Added open tag to default expand open (#42824)
By default, none of the 4 methods are expanded. this will expand the top
item by default for users.

<img width="974" height="414" alt="image"
src="https://github.com/user-attachments/assets/f328dee6-9206-47a1-b45f-1f951142eef4"
/>
2025-10-23 22:07:16 +02:00
Jaylyn Barbee
c6c7bfb861 Updated installer hashes for 0.95.1 (#42820)
Title, updating installer links for next release.
2025-10-23 12:18:45 -04:00
Mike Hall
d64f06906c Enable switching to and from MousePointerCrosshairs and Gliding Cursor (#42105)
## Summary of the Pull Request
This PR enables a user to switch between Mouse Pointer Crosshairs and
Gliding Cursor (or the other way round!). The primary change is to the
underlying state machine that's shared between Mouse Pointer Crosshairs
and Gliding Cursor, both are implemented in the same Mouse Module.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments
See above - this is primarily a change to the shared state machine
between Mouse Pointer Crosshairs and Gliding Cursor - this change
enables transition between Mouse Pointer Crosshairs and Gliding Cursor,
the underlying state is reset when a user transitions from Gliding to
Mouse Pointer and back again.

## Validation Steps Performed
Validation on a Windows Surface Laptop 7 Pro for the following states.
- Mouse Pointer Crosshairs and Gliding Cursor NOT active -
enable/disable Mouse Pointer Crosshairs
- Mouse Pointer Crosshairs and Gliding Cursor NOT active - enable/step
states for Gliding Cursor
- Activate and disable Mouse Pointer Crosshairs
- Activate and step through Gliding Cursor
- Mouse Pointer Crosshairs Active - Switch to Gliding Cursor
- Gliding Cursor Active - Switch to Mouse Pointer Crosshairs
2025-10-23 18:21:01 +08:00
Kai Tao
c26dfef81b Find My Mouse: Cursor should not go busy & window should not be active (#42795)
<!-- 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 two issue in find my mouse:
 #42758  #42765 
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #42758 and #42765
- [ ] **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
| Problem | Before | After this fix |
|----------|---------|----------------|
| Mouse Loading status | html<br><video
src="https://github.com/user-attachments/assets/a07b3fe7-a9e1-4176-a33f-4836bf7f7e0a"
controls width="300"></video> | html<br><video
src="https://github.com/user-attachments/assets/a78848b0-6f34-4d68-b470-a4a3adbd8a5e"
controls width="300"></video> |
| Current window lose focus | html<br><video
src="https://github.com/user-attachments/assets/5122262c-6c98-4fe5-a107-5db2d96a6341"
controls width="300"></video> | html<br><video
src="https://github.com/user-attachments/assets/b12391c8-d148-4498-b37f-c8c1bc9e381a"
controls width="300"></video> |


The window lose focus test: Currently after activate the find my mouse,
the window lose focus, after the fix, foreground window is still the
focused window, And my keystroke will directly apply in the foreground
window.
Maybe hard to see in the video.
2025-10-23 16:20:40 +08:00
Mason Bergstrom
dd420509ab [Mouse Without Borders] Adding Horizontal Scrolling Support (#42179)
## Summary of the Pull Request
Added support for horizontal scrolling to Mouse Without Borders, instead
of being a no-op.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments
Works in a backward compatible fashion, continuing to be a no-op when
forwarded to an older version, but works once both devices are updated.

## Validation Steps Performed
Built on two separate devices that are paired with each other. First
tested with one device updated and one on the old code, confirming
backwards compatibility support. Second tested both devices updated,
confirming horizontal scroll is now working on remote device.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 12:06:33 +08:00
Jaylyn Barbee
2c4aab9d87 Light Switch Hotfixes v2 (#42774)
Should fix: #42627
Issue: Suntimes not updating within the day if the mode changes to
SunsetToSunrise
Fix: Update suntimes in the service if the mode is changed to Sun mode.

Other: small bug fixes (brackets, etc)
2025-10-22 11:59:36 -07:00
Dustin L. Howett
52ce33d438 build: run all builds out of a new (huge) P:\ drive (#42739)
The new agents have much smaller temporary disks (which I verified with
another test run to clock in at about 87GB). For right now, let's move
our release builds to a larger drive while we troubleshoot.
2025-10-21 18:32:34 -05:00
Gordon Lam
fc1307418e Update Copilot guidance and PR prompt workflow (This PR summary and commit are generated accordingly) (#42634)
## Summary of the Pull Request
- Added new Copilot agent prompts in
`.github/prompts/create-commit-title.prompt.md`,
`.github/prompts/create-pr.prompt.md`, and
`.github/prompts/fix-spelling.prompt.md` to streamline contributor
workflows.
- Refreshed `.github/copilot-instructions.md` with front matter and
clarified build/test guidance.
And added a new section titled `Language Style Rules` to help reduce
style issues in auto-generated code.

## Screenshot
a. For the new prompts, in VSCode, just type "/" and its name
<img width="477" height="124" alt="image"
src="https://github.com/user-attachments/assets/37c7330d-6fdc-45b4-9030-95920920c964"
/>
<img width="446" height="105" alt="image"
src="https://github.com/user-attachments/assets/55d3bb49-46cc-4441-9fb5-fed3a22983d9"
/>
b. All the spelling check problem, PR description, Commit title, are
generated and fixed by above prompts as demonstration (Except this
section)

## PR Checklist
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** N/A on doc updates.
- [x] **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 (N/A)

## Detailed Description of the Pull Request / Additional comments
- Introduced prompt definitions so local tooling can generate commit
titles, PR descriptions, and resolve spell-check alerts using consistent
instructions.
- Added GitHub CLI prerequisites inside
`.github/prompts/fix-spelling.prompt.md` to ensure `gh` commands succeed
for new contributors.
- Converted `.github/copilot-instructions.md` to YAML-front-matter
format while aligning headings and terminology with current contributor
guidance.

## Validation Steps Performed
- No automated tests were run; changes are documentation and
configuration only.
2025-10-21 16:05:16 +08:00
Jaylyn Barbee
f45d54abdf [Light Switch] Hotfixes (#42434)
<!-- 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
Adds new "Off" mode for the schedule mode options which disable the
schedule.
Adds explicit function to disable light switch by default.

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

Closes: 
- #42402 
New behavior: "Off" mode added. when off, the regular service loop stops
and all actions are event driven to either resume the loop or listen for
hotkey.
- #42386
New behavior: Disabled explicitly by default
- #42389
New behavior: When switching from dark to light mode the system theme
will remove the accent color.
- #42513
New behavior: Manual mode no longer gets reset. It was being overridden
by the sun calculations that were invertedly running when in manual
mode.

Todo:
- [ ] **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 @alvinashcraft we will need to add this new mode
to the documentation.

## Validation Steps Performed
- Removed all default settings and tested new logic. Light Switch is set
to off by default.
- Updated UI and tested new "Off" mode, logs indicate mode switched and
ticker stopped. Polling resumes on mode change. (need to check that the
shortcut still works)

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
2025-10-20 15:57:03 -07:00
116 changed files with 2936 additions and 904 deletions

View File

@@ -94,6 +94,7 @@ ASSOCSTR
ASYNCWINDOWPLACEMENT
ASYNCWINDOWPOS
atl
ATX
ATRIOX
aumid
Authenticode
@@ -113,6 +114,7 @@ azman
bbwe
BCIE
bck
backticks
BESTEFFORT
bezelled
bhid
@@ -268,6 +270,7 @@ countof
covrun
cpcontrols
cph
cppcoreguidelines
cplusplus
CPower
cpptools
@@ -513,6 +516,7 @@ FARPROC
fdx
fesf
FFFF
Figma
FILEEXPLORER
fileexploreraddons
fileexplorerpreview
@@ -747,7 +751,7 @@ INITDIALOG
INITGUID
INITTOLOGFONTSTRUCT
INLINEPREFIX
Inlines
inlines
INPC
inproc
INPUTHARDWARE

View File

@@ -1,43 +1,59 @@
# PowerToys Copilot guide (concise)
---
description: PowerToys AI contributor guidance.
applyTo: pullRequests
---
# PowerToys - Copilot guide (concise)
This is the top-level guide for AI changes. Keep edits small, follow existing patterns, and cite exact paths in PRs.
Repo map (1line per area)
# Repo map (1-line per area)
- Core apps: `src/runner/**` (tray/loader), `src/settings-ui/**` (Settings app)
- Shared libs: `src/common/**`
- Modules: `src/modules/*` (one per utility; Command Palette in `src/modules/cmdpal/**`)
- Build tools/docs: `tools/**`, `doc/devdocs/**`
Build and test (defaults)
# Build and test (defaults)
- Prerequisites: Visual Studio 2022 17.4+, minimal Windows 10 1803+.
- Build discipline:
- One terminal per operation (build test). Dont switch/open new ones mid-flow.
- One terminal per operation (build -> test). Do not switch or open new ones mid-flow.
- After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`).
- Use script(s) to build, synchronously block and wait in foreground for it to finish: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages)
- Treat build **exit code 0** as success; any non-zero exit code is a failure, have Copilot read the errors log in the build folder (e.g., `build.*.*.errors.log`) and surface problems.
- Dont start tests or launch Runner until the previous step succeeded.
- Tests (fast + targeted):
- Find the test project by product code prefix (e.g., FancyZones, AdvancedPaste). Look for a sibling folder or 12 levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
- Build the test project, wait for **exit**, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
- Add/adjust tests when changing behavior; if skipped, state why (e.g., comment-only, string rename).
- Use scripts to build, synchronously block and wait in foreground for completion: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages).
- Treat build exit code 0 as success; any non-zero exit code is a failure. Read the errors log in the build folder (such as `build.*.*.errors.log`) and surface problems.
- Do not start tests or launch Runner until the previous step succeeded.
- Tests (fast and targeted):
- Find the test project by product code prefix (for example FancyZones, AdvancedPaste). Look for a sibling folder or one to two levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
- Build the test project, wait for exit, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
- Add or adjust tests when changing behavior; if skipped, state why (for example comment-only or string rename).
Pull requests (expectations)
- Atomic: one logical change; no driveby refactors.
- Describe: problem / approach / risk / test evidence.
# Pull requests (expectations)
- Atomic: one logical change; no drive-by refactors.
- Describe: problem, approach, risk, test evidence.
- List: touched paths if not obvious.
When to ask for clarification
# When to ask for clarification
- Ambiguous spec after scanning relevant docs (see below).
- Cross-module impact (shared enum/struct) not clear.
- Security / elevation / installer changes.
- Cross-module impact (shared enum or struct) not clear.
- Security, elevation, or installer changes.
Logging (use existing stacks)
- C++: `src/common/logger/**` (`Logger::info|warn|error|debug`). Keep hot paths quiet (hooks, tight loops).
- C#: `ManagedCommon.Logger` (`LogInfo|LogWarning|LogError|LogDebug|LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
# Logging (use existing stacks)
- C++ logging lives in `src/common/logger/**` (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`). Keep hot paths quiet (hooks, tight loops).
- C# logging goes through `ManagedCommon.Logger` (`LogInfo`, `LogWarning`, `LogError`, `LogDebug`, `LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
Docs to consult
# Docs to consult
- `tools/build/BUILD-GUIDELINES.md`
- `doc/devdocs/core/architecture.md`, `doc/devdocs/core/runner.md`, `doc/devdocs/core/settings/readme.md`, `doc/devdocs/modules/readme.md`
- `doc/devdocs/core/architecture.md`
- `doc/devdocs/core/runner.md`
- `doc/devdocs/core/settings/readme.md`
- `doc/devdocs/modules/readme.md`
Done checklist (self review before finishing)
- Build clean? Tests updated/passed? No unintended formatting? Any new dependency? Documented skips?
# Language style rules
- Always enforce repo analyzers: root `.editorconfig` plus any `stylecop.json`.
- C# code follows StyleCop.Analyzers and Microsoft.CodeAnalysis.NetAnalyzers.
- C++ code honors `.clang-format` plus `.clang-tidy` (modernize/cppcoreguidelines/readability).
- Markdown files wrap at 80 characters and use ATX headers with fenced code blocks that include language tags.
- YAML files indent two spaces and add comments for complex settings while keeping keys clear.
- PowerShell scripts use Verb-Noun names and prefer single-quoted literals while documenting parameters and satisfying PSScriptAnalyzer.
# Done checklist (self review before finishing)
- Build clean? Tests updated or passed? No unintended formatting? Any new dependency? Documented skips?

View File

@@ -0,0 +1,16 @@
---
mode: 'agent'
model: GPT-5-Codex (Preview)
description: 'Generate an 80-character git commit title for the local diff.'
---
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
**Workflow:**
1. Run a single command to view the local diff since the last commit:
```@terminal
git diff HEAD
```
2. From that diff, identify the dominant area (reference key paths like `src/modules/*`, `doc/devdocs/**`, etc.), the type of change (bug fix, docs update, config tweak), and any notable impact.
3. Draft a concise, imperative commit title summarizing the dominant change. Keep it plain ASCII, <= 80 characters, and avoid trailing punctuation. Mention the primary component when obvious (for example `FancyZones:` or `Docs:`).
4. Respond with only the final commit title on a single line so it can be pasted directly into `git commit`.

View File

@@ -0,0 +1,22 @@
---
mode: 'agent'
model: GPT-5-Codex (Preview)
description: 'Generate a PowerToys-ready pull request description from the local diff.'
---
**Goal:** Produce a ready-to-paste PR title and description that follows PowerToys conventions by comparing the current branch against a user-selected target branch.
**Repo guardrails:**
- Treat `.github/pull_request_template.md` as the single source of truth; load it at runtime instead of embedding hardcoded content in this prompt.
- Preserve section order from the template but only surface checklist lines that are relevant for the detected changes, filling them with `[x]`/`[ ]` as appropriate.
- Cite touched paths with inline backticks, matching the guidance in `.github/copilot-instructions.md`.
- Call out test coverage explicitly: list automated tests run (unit/UI) or state why they are not applicable.
**Workflow:**
1. Determine the target branch from user context; default to `main` when no branch is supplied.
2. Run `git status --short` once to surface uncommitted files that may influence the summary.
3. Run `git diff <target-branch>...HEAD` a single time to review the detailed changes. Only when confidence stays low dig deeper with focused calls such as `git diff <target-branch>...HEAD -- <path>`.
4. From the diff, capture impacted areas, key file changes, behavioral risks, migrations, and noteworthy edge cases.
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.

22
.github/prompts/fix-spelling.prompt.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
mode: 'agent'
model: GPT-5-Codex (Preview)
description: 'Resolve Code scanning / check-spelling comments on the active PR.'
---
**Goal:** Clear every outstanding GitHub pull request comment created by the `Code scanning / check-spelling` workflow by explicitly allowing intentional terms.
**Guardrails:**
- Update only discussion threads authored by `github-actions` or `github-actions[bot]` that mention `Code scanning results / check-spelling`.
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
- Leave all other files and topics untouched.
**Prerequisites:**
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
- Run `gh auth login` once before the first CLI use.
**Workflow:**
1. Determine the active pull request with a single `gh pr view --json number` call (default to the current branch).
2. Fetch all PR discussion data once via `gh pr view --json comments,reviews` and filter to check-spelling comments authored by `github-actions` or `github-actions[bot]` that are not minimized; when several remain, process only the most recent comment body.
3. For each flagged token, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
4. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.

View File

@@ -73,10 +73,11 @@ extends:
parameters:
pool:
name: SHINE-INT-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
${{ else }}:
image: SHINE-VS17-Latest
demands:
# Our INT agents have a large disk mounted at P:\
- WorkFolder -equals P:\_work
- ${{ if eq(parameters.useVSPreview, true) }}:
- ImageOverride -equals SHINE-VS17-Preview
os: windows
variables:
IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations

View File

@@ -1,6 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Hybrid CRT configuration -->
<PropertyGroup Condition="'$(HybridCrtConfiguration)'==''">
<HybridCrtConfiguration>$(Configuration)</HybridCrtConfiguration>
</PropertyGroup>
<!-- Skip Hybrid CRT for AppContainer/UWP projects as they require MultiThreadedDLL -->
<PropertyGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop'">
<HybridCrtConfiguration></HybridCrtConfiguration>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Debug'">
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Release'">
<ClCompile>
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<!-- AppContainer/UWP projects must use MultiThreadedDLL -->
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Debug'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Release'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<!-- Project configurations -->
<ItemGroup Label="ProjectConfigurations">
@@ -73,7 +123,6 @@
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
@@ -83,7 +132,6 @@
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>

View File

@@ -48,7 +48,7 @@ Before you begin, make sure your device meets the system requirements:
Choose one of the installation methods below:
<details>
<details open>
<summary>Download .exe from GitHub</summary>
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
@@ -56,17 +56,17 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
<!-- 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.96%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-arm64.exe
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.95.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.95.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.95.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.95.0-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
</details>
@@ -281,4 +281,4 @@ The application logs basic diagnostic data (telemetry). For more privacy informa
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs

View File

@@ -38,7 +38,6 @@
<OutDir Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup\</OutDir>
<IntDir Condition=" '$(PerUser)' != 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\</IntDir>
<IntDir Condition=" '$(PerUser)' == 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\</IntDir>
<!-- The CMD script below checks this value, and it is **CASE SENSITIVE** -->
<NormalizedPerUserValue>false</NormalizedPerUserValue>
<NormalizedPerUserValue Condition=" '$(PerUser)' == 'true' ">true</NormalizedPerUserValue>
</PropertyGroup>
@@ -115,7 +114,6 @@
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
@@ -128,7 +126,6 @@
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
</ClCompile>
<Link>

View File

@@ -1,4 +1,5 @@
<Project>
<Import Project="..\..\Directory.Build.props" Condition="Exists('..\..\Directory.Build.props')" />
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
<PropertyGroup>
<!-- Set BaseIntermediateOutputPath for each project to avoid conflicts -->
@@ -8,4 +9,4 @@
<!-- Set MSBuildProjectExtensionsPath to use the BaseIntermediateOutputPath -->
<MSBuildProjectExtensionsPath Condition="'$(BaseIntermediateOutputPath)' != ''">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
</PropertyGroup>
</Project>
</Project>

View File

@@ -1,30 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}</ProjectGuid>
<ConfigurationType>DynamicLibrary</ConfigurationType>
<CharacterSet>Unicode</CharacterSet>
<TargetName>SilentFilesInUseBAFunction</TargetName>
<ProjectName>PowerToysSetupCustomActionsVNext</ProjectName>
<ProjectModuleDefinitionFile>bafunctions.def</ProjectModuleDefinitionFile>
@@ -33,7 +10,6 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<!-- Configuration-specific property groups -->
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
@@ -65,7 +41,10 @@
</PropertyGroup>
<ItemGroup>
<ClCompile Include="SilentFilesInUseBAFunctions.cpp" />
<ClCompile Include="SilentFilesInUseBAFunctions.cpp">
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
</ClCompile>
<ClCompile Include="bafunctions.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
@@ -92,31 +71,5 @@
</Link>
</ItemDefinitionGroup>
<!-- C++ source compile-specific things for Debug/Release configurations -->
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -18,7 +18,6 @@ public: // IBootstrapperApplication
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running detect begin BA function. fCached=%d, registrationType=%d, cPackages=%u, fCancel=%d", fCached, registrationType, cPackages, *pfCancel);
LExit:
return hr;
}
@@ -32,12 +31,6 @@ public: // IBAFunctions
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running plan begin BA function. cPackages=%u, fCancel=%d", cPackages, *pfCancel);
//-------------------------------------------------------------------------------------------------
// YOUR CODE GOES HERE
// BalExitOnFailure(hr, "Change this message to represent real error handling.");
//-------------------------------------------------------------------------------------------------
LExit:
return hr;
}
@@ -63,6 +56,7 @@ public: // IBAFunctions
)
{
HRESULT hr = S_OK;
UNREFERENCED_PARAMETER(source);
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION CALLED *** Running OnExecuteFilesInUse BA function. packageId=%ls, cFiles=%u, recommendation=%d", wzPackageId, cFiles, nRecommendation);

View File

@@ -36,7 +36,7 @@
</Capabilities>
<Applications>
<Application Id="PowerToys.OCR" Executable="PowerToys.PowerOCR.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
<Application Id="PowerToys.OCR" Executable="PowerToys.PowerOCR.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.OCR"
Description="PowerToys OCR Module"
@@ -45,7 +45,7 @@
Square44x44Logo="Images\Square44x44Logo.png">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.SettingsUI" Executable="PowerToys.Settings.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.SettingsUI"
Description="PowerToys Settings UI"
@@ -54,7 +54,7 @@
Square44x44Logo="Images\Square44x44Logo.png">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.ImageResizer"
Description="PowerToys Image Resizer UI"

View File

@@ -17,7 +17,7 @@
<CIBuildParam Condition="'$(CIBuild)' != 'true'"></CIBuildParam>
</PropertyGroup>
<Exec Command="powershell -NonInteractive -ExecutionPolicy Bypass -File &quot;$(MSBuildThisFileDirectory)BuildSparsePackage.ps1&quot; -Platform $(Platform) -Configuration $(Configuration) $(NoSignParam) $(CIBuildParam)"
<Exec Command="pwsh -NonInteractive -ExecutionPolicy Bypass -File &quot;$(MSBuildThisFileDirectory)BuildSparsePackage.ps1&quot; -Platform $(Platform) -Configuration $(Configuration) $(NoSignParam) $(CIBuildParam)"
ContinueOnError="false"
WorkingDirectory="$(MSBuildThisFileDirectory)" />
</Target>

View File

@@ -9,12 +9,6 @@
<RootNamespace>CalculatorEngineCommon</RootNamespace>
<AppxPackage>false</AppxPackage>
</PropertyGroup>
<!-- BEGIN common.build.pre.props -->
<PropertyGroup Label="Configuration">
<EnableHybridCRT>true</EnableHybridCRT>
<UseCrtSDKReferenceStaticWarning Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReferenceStaticWarning>
</PropertyGroup>
<!-- END common.build.pre.props -->
<!-- BEGIN cppwinrt.build.pre.props -->
<PropertyGroup Label="Globals">
<CppWinRTEnabled>true</CppWinRTEnabled>
@@ -25,11 +19,9 @@
</PropertyGroup>
<PropertyGroup>
<MinimalCoreWin>true</MinimalCoreWin>
<AppContainerApplication>true</AppContainerApplication>
<AppContainerApplication>false</AppContainerApplication>
<WindowsStoreApp>true</WindowsStoreApp>
<ApplicationType>Windows Store</ApplicationType>
<UseCrtSDKReference Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReference>
<!-- The SDK reference breaks the Hybrid CRT -->
</PropertyGroup>
<PropertyGroup>
<!-- We have to use the Desktop platform for Hybrid CRT to work. -->
@@ -148,43 +140,5 @@
</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>
<!-- BEGIN common.build.post.props -->
<!--
The Hybrid CRT model statically links the runtime and STL and dynamically
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
WinAppSDK asserts that this is "supported according to the CRT maintainer."
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
-->
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
<ClCompile>
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<!-- END common.build.post.props -->
</Target> <!-- END common.build.post.props -->
</Project>

View File

@@ -130,7 +130,7 @@ namespace ManagedCommon
{
exMessage +=
"Inner exception: " + Environment.NewLine +
ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
ex.InnerException.GetType() + " (" + ex.InnerException.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
}
exMessage +=

View File

@@ -63,14 +63,12 @@
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>

View File

@@ -46,16 +46,6 @@
<PropertyGroup>
<TargetName>notifications</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>

View File

@@ -82,8 +82,6 @@
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">MultiThreadedDebug</RuntimeLibrary>
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
@@ -95,8 +93,6 @@
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">MultiThreaded</RuntimeLibrary>
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>

View File

@@ -20,7 +20,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<TitleBar x:Name="titleBar" IsTabStop="False">
<!-- 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

View File

@@ -39,7 +39,6 @@
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</ClCompile>
@@ -49,7 +48,6 @@
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>

View File

@@ -20,7 +20,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<TitleBar x:Name="titleBar" IsTabStop="False">
<!-- 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

View File

@@ -20,7 +20,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<TitleBar x:Name="titleBar" IsTabStop="False">
<!-- 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

View File

@@ -3,6 +3,26 @@
#include "ThemeHelper.h"
// Controls changing the themes.
static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = 0; // back to default value
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetAppsTheme(bool mode)
{
@@ -36,6 +56,11 @@ void SetSystemTheme(bool mode)
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);

View File

@@ -47,6 +47,7 @@ const static wchar_t* MODULE_DESC = L"This is a module that allows you to contro
enum class ScheduleMode
{
Off,
FixedHours,
SunsetToSunrise,
// add more later
@@ -59,8 +60,9 @@ inline std::wstring ToString(ScheduleMode mode)
case ScheduleMode::SunsetToSunrise:
return L"SunsetToSunrise";
case ScheduleMode::FixedHours:
default:
return L"FixedHours";
default:
return L"Off";
}
}
@@ -68,7 +70,9 @@ inline ScheduleMode FromString(const std::wstring& str)
{
if (str == L"SunsetToSunrise")
return ScheduleMode::SunsetToSunrise;
return ScheduleMode::FixedHours;
if (str == L"FixedHours")
return ScheduleMode::FixedHours;
return ScheduleMode::Off;
}
// These are the properties shown in the Settings page.
@@ -76,7 +80,7 @@ struct ModuleSettings
{
bool m_changeSystem = true;
bool m_changeApps = true;
ScheduleMode m_scheduleMode = ScheduleMode::FixedHours;
ScheduleMode m_scheduleMode = ScheduleMode::Off;
int m_lightTime = 480;
int m_darkTime = 1200;
int m_sunrise_offset = 0;
@@ -161,7 +165,8 @@ public:
L"scheduleMode",
L"Theme schedule mode",
ToString(g_settings.m_scheduleMode),
{ { L"FixedHours", L"Set hours manually" },
{ { L"Off", L"Disable the schedule" },
{ L"FixedHours", L"Set hours manually" },
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
// Integer spinners
@@ -284,9 +289,20 @@ public:
g_settings.m_changeApps = *v;
}
auto previousMode = g_settings.m_scheduleMode;
if (auto v = values.get_string_value(L"scheduleMode"))
{
g_settings.m_scheduleMode = FromString(*v);
auto newMode = FromString(*v);
if (newMode != g_settings.m_scheduleMode)
{
Logger::info(L"[LightSwitchInterface] Schedule mode changed from {} to {}",
ToString(g_settings.m_scheduleMode),
ToString(newMode));
g_settings.m_scheduleMode = newMode;
start_service_if_needed();
}
}
if (auto v = values.get_int_value(L"lightTime"))
@@ -304,7 +320,7 @@ public:
g_settings.m_sunrise_offset = *v;
}
if (auto v = values.get_int_value(L"m_sunset_offset"))
if (auto v = values.get_int_value(L"sunset_offset"))
{
g_settings.m_sunset_offset = *v;
}
@@ -326,6 +342,47 @@ public:
}
}
virtual void start_service_if_needed()
{
if (!m_process || WaitForSingleObject(m_process, 0) != WAIT_TIMEOUT)
{
Logger::info(L"[LightSwitchInterface] Starting LightSwitchService due to active schedule mode.");
enable();
}
else
{
Logger::debug(L"[LightSwitchInterface] Service already running, skipping start.");
}
}
/*virtual void stop_worker_only()
{
if (m_process)
{
Logger::info(L"[LightSwitchInterface] Stopping LightSwitchService (worker only).");
constexpr DWORD timeout_ms = 1500;
DWORD result = WaitForSingleObject(m_process, timeout_ms);
if (result == WAIT_TIMEOUT)
{
Logger::warn("Light Switch: Process didn't exit in time. Forcing termination.");
TerminateProcess(m_process, 0);
}
CloseHandle(m_process);
m_process = nullptr;
}
}*/
/*virtual void stop_service_if_running()
{
if (m_process)
{
Logger::info(L"[LightSwitchInterface] Stopping LightSwitchService due to schedule OFF.");
stop_worker_only();
}
}*/
virtual void enable()
{
m_enabled = true;
@@ -413,6 +470,12 @@ public:
return m_enabled;
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
@@ -471,6 +534,15 @@ public:
SetAppsTheme(!GetCurrentAppsTheme());
}
if (!m_manual_override_event_handle)
{
m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
if (!m_manual_override_event_handle)
{
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
}
}
if (m_manual_override_event_handle)
{
SetEvent(m_manual_override_event_handle);

View File

@@ -1,4 +1,4 @@
#include <windows.h>
#include <windows.h>
#include <tchar.h>
#include "ThemeScheduler.h"
#include "ThemeHelper.h"
@@ -11,11 +11,15 @@
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include <LightSwitchServiceObserver.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
HANDLE g_ServiceStopEvent = nullptr;
static int g_lastUpdatedDay = -1;
extern int g_lastUpdatedDay = -1;
static ScheduleMode prevMode = ScheduleMode::Off;
static std::wstring prevLat, prevLon;
static int prevMinutes = -1;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
@@ -146,7 +150,6 @@ static void update_sun_times(auto& settings)
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
}
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
@@ -159,25 +162,18 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
Logger::info(L"[LightSwitchService] Worker thread starting...");
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
// Initialize settings system
LightSwitchSettings::instance().InitFileWatcher();
// Open the manual override event created by the module interface
LightSwitchServiceObserver observer({ SettingId::LightTime,
SettingId::DarkTime,
SettingId::ScheduleMode,
SettingId::Sunrise_Offset,
SettingId::Sunset_Offset });
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) {
bool isLightActive = false;
if (lightMinutes < darkMinutes)
{
// Normal case: sunrise < sunset
isLightActive = (nowMinutes >= lightMinutes && nowMinutes < darkMinutes);
}
else
{
// Wraparound case: e.g. light at 21:00, dark at 06:00
isLightActive = (nowMinutes >= lightMinutes || nowMinutes < darkMinutes);
}
bool isLightActive = (lightMinutes < darkMinutes) ? (nowMinutes >= lightMinutes && nowMinutes < darkMinutes) : (nowMinutes >= lightMinutes || nowMinutes < darkMinutes);
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
@@ -185,110 +181,297 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
if (isLightActive)
{
if (settings.changeSystem && !isSystemCurrentlyLight)
{
SetSystemTheme(true);
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
}
if (settings.changeApps && !isAppsCurrentlyLight)
{
SetAppsTheme(true);
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
}
}
else
{
if (settings.changeSystem && isSystemCurrentlyLight)
{
SetSystemTheme(false);
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
}
if (settings.changeApps && isAppsCurrentlyLight)
{
SetAppsTheme(false);
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
}
}
};
// --- At service start: immediately honor the schedule ---
LightSwitchSettings::instance().LoadSettings();
auto& settings = LightSwitchSettings::instance().settings();
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
if (settings.scheduleMode != ScheduleMode::Off)
{
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
applyTheme(nowMinutes,
settings.lightTime + settings.sunrise_offset,
settings.darkTime + settings.sunset_offset,
settings);
Logger::trace(L"[LightSwitchService] Initialized g_lastUpdatedDay = {}", g_lastUpdatedDay);
}
else
{
Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change.");
}
// --- Main loop: wakes once per minute or stop/parent death ---
g_lastUpdatedDay = st.wDay;
ULONGLONG lastSettingsReload = 0;
for (;;)
{
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
DWORD count = hParent ? 2 : 1;
bool skipRest = false;
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
// Refresh suntimes at day boundary
if (g_lastUpdatedDay != st.wDay)
bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
prevMode = settings.scheduleMode;
// ─── Handle "Schedule Off" Mode ─────────────────────────────────────────────
if (settings.scheduleMode == ScheduleMode::Off)
{
update_sun_times(settings);
g_lastUpdatedDay = st.wDay;
Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive.");
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
}
if (!hManualOverride)
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
wchar_t msg[160];
swprintf_s(msg,
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d",
st.wHour,
st.wMinute,
settings.lightTime / 60,
settings.lightTime % 60,
settings.darkTime / 60,
settings.darkTime % 60);
Logger::info(msg);
HANDLE waitsOff[4];
DWORD countOff = 0;
waitsOff[countOff++] = g_ServiceStopEvent;
if (hParent)
waitsOff[countOff++] = hParent;
if (hManualOverride)
waitsOff[countOff++] = hManualOverride;
waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
// --- Manual override check ---
bool manualOverrideActive = false;
if (hManualOverride)
{
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
}
if (manualOverrideActive)
{
// Did we hit a scheduled boundary? (reset override at boundary)
if (nowMinutes == (settings.lightTime + settings.sunrise_offset) % 1440 ||
nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
for (;;)
{
ResetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Manual override cleared at boundary\n");
DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE);
if (wait == WAIT_OBJECT_0)
{
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
goto cleanup;
}
if (hParent && wait == WAIT_OBJECT_0 + 1)
{
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
goto cleanup;
}
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
{
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
ResetEvent(hManualOverride);
continue;
}
if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2))
{
Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings...");
ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent());
LightSwitchSettings::instance().LoadSettings();
const auto& newSettings = LightSwitchSettings::instance().settings();
lastSettingsReload = GetTickCount64();
if (newSettings.scheduleMode != ScheduleMode::Off)
{
Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop.");
break;
}
}
}
continue;
}
// ─── Normal Schedule Loop ───────────────────────────────────────────────────
ULONGLONG nowTick = GetTickCount64();
bool recentSettingsReload = (nowTick - lastSettingsReload < 5000);
if (g_lastUpdatedDay != -1)
{
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled)
{
Logger::debug(L"[LightSwitchService] Checking if manual override is active...");
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
if (!manualOverrideActive)
{
bool currentSystemTheme = GetCurrentSystemTheme();
bool currentAppsTheme = GetCurrentAppsTheme();
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
bool shouldBeLight = (settings.lightTime < settings.darkTime) ? (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime) : (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime);
Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight);
if ((settings.changeSystem && (currentSystemTheme != shouldBeLight)) ||
(settings.changeApps && (currentAppsTheme != shouldBeLight)))
{
Logger::debug(L"[LightSwitchService] External theme change detected - enabling manual override");
if (!hManualOverride)
{
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
if (!hManualOverride)
hManualOverride = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
}
if (hManualOverride)
{
SetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Detected manual theme change outside of LightSwitch. Triggering manual override.");
skipRest = true;
}
}
}
}
else
{
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override\n");
goto sleep_until_next_minute;
Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
}
}
// Apply theme logic (only runs if no manual override or override just cleared)
applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
// ─── Apply Schedule Logic ───────────────────────────────────────────────────
if (!skipRest)
{
bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
settings.scheduleMode == ScheduleMode::SunsetToSunrise);
bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
sleep_until_next_minute:
if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
{
Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
update_sun_times(settings);
SYSTEMTIME st;
GetLocalTime(&st);
g_lastUpdatedDay = st.wDay;
prevMode = settings.scheduleMode;
prevLat = settings.latitude;
prevLon = settings.longitude;
}
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise))
{
update_sun_times(settings);
g_lastUpdatedDay = st.wDay;
prevMinutes = -1;
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
}
LightSwitchSettings::instance().LoadSettings();
const auto& currentSettings = LightSwitchSettings::instance().settings();
wchar_t msg[160];
swprintf_s(msg,
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d",
st.wHour,
st.wMinute,
currentSettings.lightTime / 60,
currentSettings.lightTime % 60,
currentSettings.darkTime / 60,
currentSettings.darkTime % 60,
static_cast<int>(currentSettings.scheduleMode));
Logger::info(msg);
bool manualOverrideActive = false;
if (hManualOverride)
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
if (manualOverrideActive)
{
int lightBoundary = (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440;
int darkBoundary = (currentSettings.darkTime + currentSettings.sunset_offset) % 1440;
bool crossedLight = false;
bool crossedDark = false;
if (prevMinutes != -1)
{
if (nowMinutes < prevMinutes)
{
crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary);
crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
}
else
{
crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary);
crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
}
}
Logger::debug(L"[LightSwitchService] prevMinutes={} nowMinutes={} light={} dark={}",
prevMinutes,
nowMinutes,
lightBoundary,
darkBoundary);
if (crossedLight || crossedDark)
{
ResetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary.");
}
else
{
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override");
skipRest = true;
}
}
if (!skipRest)
applyTheme(nowMinutes,
currentSettings.lightTime + currentSettings.sunrise_offset,
currentSettings.darkTime + currentSettings.sunset_offset,
currentSettings);
}
// ─── Wait For Next Minute Tick Or Stop Event ────────────────────────────────
SYSTEMTIME st;
GetLocalTime(&st);
int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds;
if (msToNextMinute < 50)
msToNextMinute = 50;
prevMinutes = nowMinutes;
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
if (wait == WAIT_OBJECT_0)
{
Logger::info(L"[LightSwitchService] Stop event triggered <EFBFBD> exiting worker loop.");
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
break;
}
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
if (hParent && wait == WAIT_OBJECT_0 + 1)
{
Logger::info(L"[LightSwitchService] Parent process exited <EFBFBD> stopping service.");
Logger::info(L"[LightSwitchService] Parent process exited - stopping service.");
break;
}
}
cleanup:
if (hManualOverride)
CloseHandle(hManualOverride);
if (hParent)
@@ -297,6 +480,54 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
return 0;
}
void ApplyThemeNow()
{
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
bool shouldBeLight = false;
if (settings.lightTime < settings.darkTime)
shouldBeLight = (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime);
else
shouldBeLight = (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime);
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
Logger::info(L"[LightSwitchService] Applying (if needed) theme immediately due to schedule change.");
if (shouldBeLight)
{
if (settings.changeSystem && !isSystemCurrentlyLight)
{
SetSystemTheme(true);
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
}
if (settings.changeApps && !isAppsCurrentlyLight)
{
SetAppsTheme(true);
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
}
}
else
{
if (settings.changeSystem && isSystemCurrentlyLight)
{
SetSystemTheme(false);
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
}
if (settings.changeApps && isAppsCurrentlyLight)
{
SetAppsTheme(false);
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
}
}
}
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)

View File

@@ -74,6 +74,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchServiceObserver.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
@@ -84,6 +85,7 @@
<ResourceCompile Include="LightSwitchService.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="LightSwitchServiceObserver.h" />
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />

View File

@@ -33,6 +33,9 @@
<ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LightSwitchServiceObserver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
@@ -53,6 +56,9 @@
<ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LightSwitchServiceObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -0,0 +1,29 @@
#include "LightSwitchServiceObserver.h"
#include <logger.h>
#include "LightSwitchSettings.h"
// These are defined elsewhere in your service module (ServiceWorkerThread.cpp)
extern int g_lastUpdatedDay;
void ApplyThemeNow();
void LightSwitchServiceObserver::SettingsUpdate(SettingId id)
{
Logger::info(L"[LightSwitchService] Setting changed: {}", static_cast<int>(id));
g_lastUpdatedDay = -1;
ApplyThemeNow();
}
bool LightSwitchServiceObserver::WantsToBeNotified(SettingId id) const noexcept
{
switch (id)
{
case SettingId::LightTime:
case SettingId::DarkTime:
case SettingId::ScheduleMode:
case SettingId::Sunrise_Offset:
case SettingId::Sunset_Offset:
return true;
default:
return false;
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "SettingsObserver.h"
// The LightSwitchServiceObserver reacts when LightSwitchSettings changes.
class LightSwitchServiceObserver : public SettingsObserver
{
public:
explicit LightSwitchServiceObserver(std::unordered_set<SettingId> observedSettings) :
SettingsObserver(std::move(observedSettings))
{
}
void SettingsUpdate(SettingId id) override;
bool WantsToBeNotified(SettingId id) const noexcept override;
};

View File

@@ -6,6 +6,7 @@
#include <filesystem>
#include <fstream>
#include <WinHookEventIDs.h>
#include <logger.h>
using namespace std;
@@ -27,10 +28,21 @@ std::wstring LightSwitchSettings::GetSettingsFileName()
void LightSwitchSettings::InitFileWatcher()
{
const std::wstring& settingsFileName = GetSettingsFileName();
m_settingsFileWatcher = std::make_unique<FileWatcher>(settingsFileName, [&]() {
PostMessageW(HWND_BROADCAST, WM_PRIV_SETTINGS_CHANGED, NULL, NULL);
});
if (!m_settingsChangedEvent)
{
m_settingsChangedEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
}
if (!m_settingsFileWatcher)
{
m_settingsFileWatcher = std::make_unique<FileWatcher>(
GetSettingsFileName(),
[this]() {
Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event.");
LoadSettings();
SetEvent(m_settingsChangedEvent);
});
}
}
void LightSwitchSettings::AddObserver(SettingsObserver& observer)
@@ -54,6 +66,11 @@ void LightSwitchSettings::NotifyObservers(SettingId id) const
}
}
HANDLE LightSwitchSettings::GetSettingsChangedEvent() const
{
return m_settingsChangedEvent;
}
void LightSwitchSettings::LoadSettings()
{
try

View File

@@ -14,6 +14,7 @@ class SettingsObserver;
enum class ScheduleMode
{
Off,
FixedHours,
SunsetToSunrise
// Add more in the future
@@ -28,7 +29,7 @@ inline std::wstring ToString(ScheduleMode mode)
case ScheduleMode::SunsetToSunrise:
return L"SunsetToSunrise";
default:
return L"FixedHours";
return L"Off";
}
}
@@ -36,8 +37,10 @@ inline ScheduleMode FromString(const std::wstring& str)
{
if (str == L"SunsetToSunrise")
return ScheduleMode::SunsetToSunrise;
else
if (str == L"FixedHours")
return ScheduleMode::FixedHours;
else
return ScheduleMode::Off;
}
struct LightSwitchConfig
@@ -76,6 +79,8 @@ public:
void LoadSettings();
HANDLE GetSettingsChangedEvent() const;
private:
LightSwitchSettings();
~LightSwitchSettings() = default;
@@ -85,4 +90,6 @@ private:
std::unordered_set<SettingsObserver*> m_observers;
void NotifyObservers(SettingId id) const;
HANDLE m_settingsChangedEvent = nullptr;
};

View File

@@ -2,6 +2,7 @@
#include <unordered_set>
#include "SettingsConstants.h"
#include "LightSwitchSettings.h"
class LightSwitchSettings;
@@ -22,7 +23,7 @@ public:
// Override this in your class to respond to updates
virtual void SettingsUpdate(SettingId type) {}
bool WantsToBeNotified(SettingId type) const noexcept
virtual bool WantsToBeNotified(SettingId type) const noexcept
{
return m_observedSettings.contains(type);
}

View File

@@ -1,8 +1,32 @@
#include <windows.h>
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include "ThemeHelper.h"
// Controls changing the themes.
static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = 0; // back to default value
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetAppsTheme(bool mode)
{
HKEY hKey;
@@ -35,6 +59,12 @@ void SetSystemTheme(bool mode)
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);

View File

@@ -189,7 +189,7 @@ bool SuperSonar<D>::Initialize(HINSTANCE hinst)
return false;
}
DWORD exStyle = WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this);
if (!created)
{

View File

@@ -64,7 +64,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
@@ -82,7 +81,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>

View File

@@ -48,7 +48,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
@@ -66,7 +65,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>

View File

@@ -48,7 +48,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
@@ -66,7 +65,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>

View File

@@ -49,7 +49,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
@@ -67,7 +66,6 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>

View File

@@ -14,6 +14,9 @@ extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn();
extern void InclusiveCrosshairsEnsureOff();
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
extern void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation);
extern bool InclusiveCrosshairsIsEnabled();
extern void InclusiveCrosshairsSwitch();
// Non-Localizable strings
namespace
@@ -244,12 +247,19 @@ public:
return false;
}
if (hotkeyId == 0)
if (hotkeyId == 0) // Crosshairs activation
{
// If gliding cursor is active, cancel it and activate crosshairs
if (m_glideState.load() != 0)
{
CancelGliding(true /*activateCrosshairs*/);
return true;
}
// Otherwise, normal crosshairs toggle
InclusiveCrosshairsSwitch();
return true;
}
if (hotkeyId == 1)
if (hotkeyId == 1) // Gliding cursor activation
{
HandleGlidingHotkey();
return true;
@@ -268,25 +278,44 @@ private:
SendInput(2, inputs, sizeof(INPUT));
}
// Cancel gliding without performing the final click (Escape handling)
void CancelGliding()
// Cancel gliding with option to activate crosshairs in user's preferred orientation
void CancelGliding(bool activateCrosshairs)
{
int state = m_glideState.load();
if (state == 0)
{
return; // nothing to cancel
}
// Stop all gliding operations
StopXTimer();
StopYTimer();
m_glideState = 0;
InclusiveCrosshairsEnsureOff();
UninstallKeyboardHook();
// Reset crosshairs control and restore user settings
InclusiveCrosshairsSetExternalControl(false);
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
if (activateCrosshairs)
{
// User is switching to crosshairs mode - enable with their settings
InclusiveCrosshairsEnsureOn();
}
else
{
// User canceled (Escape) - turn off crosshairs completely
InclusiveCrosshairsEnsureOff();
}
// Reset gliding state
if (auto s = m_state)
{
s->xFraction = 0.0;
s->yFraction = 0.0;
}
Logger::debug("Gliding cursor cancelled via Escape key");
Logger::debug("Gliding cursor cancelled (activateCrosshairs={})", activateCrosshairs ? 1 : 0);
}
// Stateless helpers operating on shared State
@@ -425,21 +454,22 @@ private:
{
return;
}
// Simulate the AHK state machine
int state = m_glideState.load();
switch (state)
{
case 0:
case 0: // Starting gliding
{
// For detect for cancel key
// Install keyboard hook for Escape cancellation
InstallKeyboardHook();
// Ensure crosshairs on (do not toggle off if already on)
InclusiveCrosshairsEnsureOn();
// Disable internal mouse hook so we control position updates explicitly
// Force crosshairs visible in BOTH orientation for gliding, regardless of user setting
// Set external control before enabling to prevent internal movement hook from attaching
InclusiveCrosshairsSetExternalControl(true);
// Override crosshairs to show both for Gliding Cursor
InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
InclusiveCrosshairsEnsureOn(); // Always ensure they are visible
// Initialize gliding state
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
s->xFraction = 0.0;
@@ -447,20 +477,17 @@ private:
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
SetCursorPos(0, y);
InclusiveCrosshairsRequestUpdatePosition();
m_glideState = 1;
StartXTimer();
break;
}
case 1:
{
// Slow horizontal
case 1: // Slow horizontal
s->currentXSpeed = s->slowHSpeed;
m_glideState = 2;
break;
}
case 2:
case 2: // Switch to vertical fast
{
// Stop horizontal, start vertical (fast)
StopXTimer();
s->currentYSpeed = s->fastVSpeed;
s->currentYPos = 0;
@@ -471,33 +498,37 @@ private:
StartYTimer();
break;
}
case 3:
{
// Slow vertical
case 3: // Slow vertical
s->currentYSpeed = s->slowVSpeed;
m_glideState = 4;
break;
}
case 4:
case 4: // Finalize (click and end)
default:
{
UninstallKeyboardHook();
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
// Complete the gliding sequence
StopYTimer();
m_glideState = 0;
LeftClick();
InclusiveCrosshairsEnsureOff();
// Restore normal crosshairs operation and turn them off
InclusiveCrosshairsSetExternalControl(false);
// Restore original crosshairs orientation setting
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
s->xFraction = 0.0;
s->yFraction = 0.0;
InclusiveCrosshairsEnsureOff();
UninstallKeyboardHook();
// Reset state
if (auto sp = m_state)
{
sp->xFraction = 0.0;
sp->yFraction = 0.0;
}
break;
}
}
}
// Low-level keyboard hook procedures
// Low-level keyboard hook for Escape cancellation
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION)
@@ -509,14 +540,11 @@ private:
{
if (inst->m_enabled && inst->m_glideState.load() != 0)
{
inst->UninstallKeyboardHook();
inst->CancelGliding();
inst->CancelGliding(false); // Escape cancels without activating crosshairs
}
}
}
}
// Do not swallow Escape; pass it through
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}

View File

@@ -112,6 +112,7 @@ namespace MouseWithoutBorders
internal const int WM_RBUTTONDBLCLK = 0x206;
internal const int WM_MBUTTONDBLCLK = 0x209;
internal const int WM_MOUSEWHEEL = 0x020A;
internal const int WM_MOUSEHWHEEL = 0x020E;
internal const int WM_KEYDOWN = 0x100;
internal const int WM_KEYUP = 0x101;

View File

@@ -204,6 +204,9 @@ namespace MouseWithoutBorders.Class
case Common.WM_MOUSEWHEEL:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.WHEEL;
break;
case Common.WM_MOUSEHWHEEL:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.HWHEEL;
break;
case Common.WM_XBUTTONUP:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XUP;
break;

View File

@@ -556,6 +556,7 @@ namespace MouseWithoutBorders.Class
XDOWN = 0x0080,
XUP = 0x0100,
WHEEL = 0x0800,
HWHEEL = 0x1000,
VIRTUALDESK = 0x4000,
ABSOLUTE = 0x8000,
}

View File

@@ -67,8 +67,6 @@
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>dll.def</ModuleDefinitionFile>
<AdditionalDependencies>runtimeobject.lib;$(CoreLibraryDependencies)</AdditionalDependencies>
<IgnoreSpecificDefaultLibraries>
</IgnoreSpecificDefaultLibraries>
</Link>
<PreBuildEvent>
<Command>del $(OutDir)\NewPlusPackage.msix /q
@@ -100,8 +98,6 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>dll.def</ModuleDefinitionFile>
<AdditionalDependencies>runtimeobject.lib;$(CoreLibraryDependencies)</AdditionalDependencies>
<IgnoreSpecificDefaultLibraries>
</IgnoreSpecificDefaultLibraries>
</Link>
<PreBuildEvent>
<Command>del $(OutDir)\NewPlusPackage.msix /q

View File

@@ -29,7 +29,6 @@
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
@@ -40,7 +39,6 @@
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>

View File

@@ -29,7 +29,6 @@
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
@@ -40,7 +39,6 @@
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>

View File

@@ -29,7 +29,6 @@
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
@@ -40,7 +39,6 @@
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>

View File

@@ -81,7 +81,6 @@
<Optimization>MaxSpeed</Optimization>
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<StringPooling>true</StringPooling>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
</ClCompile>
<ResourceCompile>
@@ -103,7 +102,6 @@
<Optimization>MaxSpeed</Optimization>
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<StringPooling>true</StringPooling>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
</ClCompile>
<ResourceCompile>
@@ -126,7 +124,6 @@
<Optimization>MaxSpeed</Optimization>
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<StringPooling>true</StringPooling>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
</ClCompile>
<ResourceCompile>
@@ -148,7 +145,6 @@
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;_M_IX86;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -169,7 +165,6 @@
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -191,7 +186,6 @@
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>

View File

@@ -29,7 +29,6 @@
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
@@ -40,7 +39,6 @@
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>

View File

@@ -141,43 +141,4 @@
<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>
<!-- BEGIN common.build.post.props -->
<!--
The Hybrid CRT model statically links the runtime and STL and dynamically
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
WinAppSDK asserts that this is "supported according to the CRT maintainer."
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
-->
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
<ClCompile>
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<!-- END common.build.post.props -->
</Project>

View File

@@ -306,6 +306,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Command.PropertyChanged -= Command_PropertyChanged;
Command = new(model.Command, PageContext);
Command.InitializeProperties();
Command.PropertyChanged += Command_PropertyChanged;
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.

View File

@@ -97,6 +97,17 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(null), PageContext);
}
private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(FiltersViewModel.Filters))
{
var filtersViewModel = sender as FiltersViewModel;
var hasFilters = filtersViewModel?.Filters.Length > 0;
HasFilters = hasFilters;
UpdateProperty(nameof(HasFilters));
}
}
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems();
@@ -586,8 +597,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties();
Filters?.PropertyChanged -= FiltersPropertyChanged;
Filters = new(new(model.Filters), PageContext);
Filters.InitializeProperties();
Filters?.PropertyChanged += FiltersPropertyChanged;
Filters?.InitializeProperties();
UpdateProperty(nameof(Filters));
FetchItems();
@@ -686,8 +700,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent.SlowInitializeProperties();
break;
case nameof(Filters):
Filters?.PropertyChanged -= FiltersPropertyChanged;
Filters = new(new(model.Filters), PageContext);
Filters.InitializeProperties();
Filters?.PropertyChanged += FiltersPropertyChanged;
Filters?.InitializeProperties();
break;
case nameof(IsLoading):
UpdateEmptyContent();
@@ -757,6 +773,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
FilteredItems.Clear();
}
Filters?.PropertyChanged -= FiltersPropertyChanged;
Filters?.SafeCleanup();
var model = _model.Unsafe;

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.Core.ViewModels.Messages;
/// <summary>
/// Used to navigate down one page in the page when pressing the PageDown key in the SearchBox.
/// </summary>
public record NavigatePageDownCommand
{
}

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.Core.ViewModels.Messages;
/// <summary>
/// Used to navigate up one page in the page when pressing the PageUp key in the SearchBox.
/// </summary>
public record NavigatePageUpCommand
{
}

View File

@@ -68,7 +68,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
// `IsLoading` property as a combo of this value and `IsInitialized`
public bool ModelIsLoading { get; protected set; } = true;
public bool HasSearchBox { get; protected set; }
public bool HasSearchBox { get; protected set; } = true;
public bool HasFilters { get; protected set; }
public IconInfoViewModel Icon { get; protected set; }

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.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -48,6 +49,9 @@ public partial class ShellViewModel : ObservableObject,
var oldValue = _currentPage;
if (SetProperty(ref _currentPage, value))
{
oldValue.PropertyChanged -= CurrentPage_PropertyChanged;
value.PropertyChanged += CurrentPage_PropertyChanged;
if (oldValue is IDisposable disposable)
{
try
@@ -63,6 +67,14 @@ public partial class ShellViewModel : ObservableObject,
}
}
private void CurrentPage_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PageViewModel.HasSearchBox))
{
IsSearchBoxVisible = CurrentPage.HasSearchBox;
}
}
private IPage? _rootPage;
private bool _isNested;

View File

@@ -0,0 +1,7 @@
// 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.UI.ViewModels.Messages;
public record ReloadFinishedMessage();

View File

@@ -410,5 +410,23 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
return ResourceManager.GetString("builtin_reload_subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} extensions found.
/// </summary>
public static string builtin_settings_extension_n_extensions_found {
get {
return ResourceManager.GetString("builtin_settings_extension_n_extensions_found", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} extensions installed.
/// </summary>
public static string builtin_settings_extension_n_extensions_installed {
get {
return ResourceManager.GetString("builtin_settings_extension_n_extensions_installed", resourceCulture);
}
}
}
}

View File

@@ -236,4 +236,10 @@
<data name="builtin_home_name" xml:space="preserve">
<value>Home</value>
</data>
<data name="builtin_settings_extension_n_extensions_found" xml:space="preserve">
<value>{0} extensions found</value>
</data>
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
<value>{0} extensions installed</value>
</data>
</root>

View File

@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Provides filtering over the list of provider settings view models.
/// Intended to be used by the UI to bind a TextBox (SearchText) and an ItemsRepeater (FilteredProviders).
/// </summary>
public partial class SettingsExtensionsViewModel : ObservableObject
{
private static readonly CompositeFormat LabelNumberExtensionFound
= CompositeFormat.Parse(Properties.Resources.builtin_settings_extension_n_extensions_found!);
private static readonly CompositeFormat LabelNumberExtensionInstalled
= CompositeFormat.Parse(Properties.Resources.builtin_settings_extension_n_extensions_installed!);
private readonly ObservableCollection<ProviderSettingsViewModel> _source;
private readonly TaskScheduler _uiScheduler;
public ObservableCollection<ProviderSettingsViewModel> FilteredProviders { get; } = [];
private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set
{
if (_searchText != value)
{
_searchText = value;
OnPropertyChanged();
ApplyFilter();
}
}
}
public string ItemCounterText
{
get
{
var hasQuery = !string.IsNullOrWhiteSpace(_searchText);
var count = hasQuery ? FilteredProviders.Count : _source.Count;
var format = hasQuery ? LabelNumberExtensionFound : LabelNumberExtensionInstalled;
return string.Format(CultureInfo.CurrentCulture, format, count);
}
}
public bool ShowManualReloadOverlay
{
get;
private set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
}
public bool ShowNoResultsPanel => !string.IsNullOrWhiteSpace(_searchText) && FilteredProviders.Count == 0;
public bool HasResults => !ShowNoResultsPanel;
public IRelayCommand ReloadExtensionsCommand { get; }
public SettingsExtensionsViewModel(ObservableCollection<ProviderSettingsViewModel> source, TaskScheduler uiScheduler)
{
_source = source;
_uiScheduler = uiScheduler;
_source.CollectionChanged += Source_CollectionChanged;
ApplyFilter();
ReloadExtensionsCommand = new RelayCommand(ReloadExtensions);
WeakReferenceMessenger.Default.Register<ReloadFinishedMessage>(this, (_, _) =>
{
Task.Factory.StartNew(() => ShowManualReloadOverlay = false, CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
});
}
private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ApplyFilter();
}
private void ApplyFilter()
{
var query = _searchText;
var filtered = ListHelpers.FilterList(_source, query, Matches);
ListHelpers.InPlaceUpdateList(FilteredProviders, filtered);
OnPropertyChanged(nameof(ItemCounterText));
OnPropertyChanged(nameof(HasResults));
OnPropertyChanged(nameof(ShowNoResultsPanel));
}
private static int Matches(string query, ProviderSettingsViewModel item)
{
if (string.IsNullOrWhiteSpace(query))
{
return 100;
}
return Contains(item.DisplayName, query)
|| Contains(item.ExtensionName, query)
|| Contains(item.ExtensionSubtext, query)
? 100
: 0;
}
private static bool Contains(string? haystack, string needle)
{
return !string.IsNullOrEmpty(haystack) && haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
}
[RelayCommand]
private void OpenStoreWithExtension(string? query)
{
const string extensionsAssocUri = "ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette";
ShellHelpers.OpenInShell(extensionsAssocUri);
}
private void ReloadExtensions()
{
ShowManualReloadOverlay = true;
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>();
}
}

View File

@@ -54,6 +54,8 @@ public partial class SettingsModel : ObservableObject
public bool DisableAnimations { get; set; } = true;
public WindowPosition? LastWindowPosition { get; set; }
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
@@ -191,6 +193,7 @@ public partial class SettingsModel : ObservableObject
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(SettingsModel))]
[JsonSerializable(typeof(WindowPosition))]
[JsonSerializable(typeof(AppStateModel))]
[JsonSerializable(typeof(RecentCommandsManager))]
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
@@ -208,4 +211,5 @@ public enum MonitorBehavior
ToPrimary = 1,
ToFocusedWindow = 2,
InPlace = 3,
ToLast = 4,
}

View File

@@ -140,6 +140,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsExtensionsViewModel Extensions { get; }
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
{
_settings = settings;
@@ -155,6 +157,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider);
CommandProviders.Add(settingsModel);
}
Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);
}
private IEnumerable<CommandProviderWrapper> GetCommandProviders()

View File

@@ -259,6 +259,9 @@ public partial class TopLevelCommandManager : ObservableObject,
IsLoading = false;
// Send on the current thread; receivers should marshal to UI if needed
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
return true;
}

View File

@@ -219,7 +219,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));
if (e.PropertyName == "IsInitialized")
if (e.PropertyName is "IsInitialized" or nameof(CommandItemViewModel.Command))
{
GenerateId();

View File

@@ -0,0 +1,22 @@
// 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;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class WindowPosition
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}

View File

@@ -0,0 +1,100 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_125_2801)">
<g clip-path="url(#clip1_125_2801)">
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint0_linear_125_2801)"/>
<mask id="mask0_125_2801" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="4" width="23" height="19">
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint1_linear_125_2801)"/>
</mask>
<g mask="url(#mask0_125_2801)">
<g filter="url(#filter0_dd_125_2801)">
<path d="M6 5V1H17V5" stroke="url(#paint2_linear_125_2801)" stroke-width="2" stroke-linecap="round"/>
</g>
</g>
<rect x="6" y="8" width="11" height="11" rx="1" fill="white" fill-opacity="0.5"/>
<g filter="url(#filter1_d_125_2801)">
<path d="M11 9H7V13H11V9Z" fill="#F25022"/>
<path d="M16 9H12V13H16V9Z" fill="#7FBA00"/>
<path d="M16 14H12V18H16V14Z" fill="#FFB900"/>
<path d="M11 14H7V18H11V14Z" fill="#00A4EF"/>
</g>
<path d="M6 5V2C6 1.44772 6.44772 1 7 1H16C16.5523 1 17 1.44772 17 2V5" stroke="url(#paint3_linear_125_2801)" stroke-width="2" stroke-linecap="round"/>
<rect x="7" width="9" height="2" fill="url(#paint4_linear_125_2801)"/>
<mask id="mask1_125_2801" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="7" y="0" width="9" height="2">
<rect x="7" width="9" height="2" fill="url(#paint5_linear_125_2801)"/>
</mask>
<g mask="url(#mask1_125_2801)">
<g filter="url(#filter2_dd_125_2801)">
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_dd_125_2801" x="3.5" y="-1" width="16" height="9" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.25"/>
<feGaussianBlur stdDeviation="0.25"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2801"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2801" result="effect2_dropShadow_125_2801"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2801" result="shape"/>
</filter>
<filter id="filter1_d_125_2801" x="6" y="8.25" width="11" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.25"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2801"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_125_2801" result="shape"/>
</filter>
<filter id="filter2_dd_125_2801" x="3.5" y="-2.5" width="16" height="8" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-1"/>
<feGaussianBlur stdDeviation="0.25"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2801"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-1"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2801" result="effect2_dropShadow_125_2801"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2801" result="shape"/>
</filter>
<linearGradient id="paint0_linear_125_2801" x1="5.22727" y1="1.19318" x2="9.5894" y2="22.315" gradientUnits="userSpaceOnUse">
<stop offset="0.270833" stop-color="white"/>
<stop offset="1" stop-color="#DFDFDF"/>
</linearGradient>
<linearGradient id="paint1_linear_125_2801" x1="11.5" y1="4" x2="15.4941" y2="23.743" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="1" stop-color="#114A8B"/>
</linearGradient>
<linearGradient id="paint2_linear_125_2801" x1="11.5" y1="1" x2="11.8823" y2="5.29249" gradientUnits="userSpaceOnUse">
<stop stop-color="#28AFEA"/>
<stop offset="1" stop-color="#0078D4"/>
</linearGradient>
<linearGradient id="paint3_linear_125_2801" x1="11.5" y1="1" x2="11.7158" y2="4.23049" gradientUnits="userSpaceOnUse">
<stop stop-color="#30DAFF"/>
<stop offset="1" stop-color="#0094D4"/>
</linearGradient>
<linearGradient id="paint4_linear_125_2801" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
<stop stop-color="#22BCFF"/>
<stop offset="1" stop-color="#0088F0"/>
</linearGradient>
<linearGradient id="paint5_linear_125_2801" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
<stop stop-color="#28AFEA"/>
<stop offset="1" stop-color="#3CCBF4"/>
</linearGradient>
<clipPath id="clip0_125_2801">
<rect width="24" height="24" fill="white"/>
</clipPath>
<clipPath id="clip1_125_2801">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,100 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_125_2875)">
<g clip-path="url(#clip1_125_2875)">
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint0_linear_125_2875)"/>
<mask id="mask0_125_2875" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="4" width="23" height="19">
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint1_linear_125_2875)"/>
</mask>
<g mask="url(#mask0_125_2875)">
<g filter="url(#filter0_dd_125_2875)">
<path d="M6 5V1H17V5" stroke="url(#paint2_linear_125_2875)" stroke-width="2" stroke-linecap="round"/>
</g>
</g>
<rect x="6" y="8" width="11" height="11" rx="1" fill="#243A5F" fill-opacity="0.5"/>
<g filter="url(#filter1_d_125_2875)">
<path d="M11 9H7V13H11V9Z" fill="#F25022"/>
<path d="M16 9H12V13H16V9Z" fill="#7FBA00"/>
<path d="M16 14H12V18H16V14Z" fill="#FFB900"/>
<path d="M11 14H7V18H11V14Z" fill="#00A4EF"/>
</g>
<path d="M6 5V2C6 1.44772 6.44772 1 7 1H16C16.5523 1 17 1.44772 17 2V5" stroke="url(#paint3_linear_125_2875)" stroke-width="2" stroke-linecap="round"/>
<rect x="7" width="9" height="2" fill="url(#paint4_linear_125_2875)"/>
<mask id="mask1_125_2875" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="7" y="0" width="9" height="2">
<rect x="7" width="9" height="2" fill="url(#paint5_linear_125_2875)"/>
</mask>
<g mask="url(#mask1_125_2875)">
<g filter="url(#filter2_dd_125_2875)">
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_dd_125_2875" x="3.5" y="-1" width="16" height="9" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.25"/>
<feGaussianBlur stdDeviation="0.25"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2875"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2875" result="effect2_dropShadow_125_2875"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2875" result="shape"/>
</filter>
<filter id="filter1_d_125_2875" x="6" y="8.25" width="11" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.25"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2875"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_125_2875" result="shape"/>
</filter>
<filter id="filter2_dd_125_2875" x="3.5" y="-2.5" width="16" height="8" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-1"/>
<feGaussianBlur stdDeviation="0.25"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2875"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-1"/>
<feGaussianBlur stdDeviation="0.75"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2875" result="effect2_dropShadow_125_2875"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2875" result="shape"/>
</filter>
<linearGradient id="paint0_linear_125_2875" x1="11.5" y1="4" x2="15.4941" y2="23.743" gradientUnits="userSpaceOnUse">
<stop stop-color="#0669BC"/>
<stop offset="1" stop-color="#243A5F"/>
</linearGradient>
<linearGradient id="paint1_linear_125_2875" x1="11.5" y1="4" x2="15.4941" y2="23.743" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="1" stop-color="#114A8B"/>
</linearGradient>
<linearGradient id="paint2_linear_125_2875" x1="11.5" y1="1" x2="11.8823" y2="5.29249" gradientUnits="userSpaceOnUse">
<stop stop-color="#28AFEA"/>
<stop offset="1" stop-color="#0078D4"/>
</linearGradient>
<linearGradient id="paint3_linear_125_2875" x1="11.5" y1="1" x2="11.7158" y2="4.23049" gradientUnits="userSpaceOnUse">
<stop stop-color="#30DAFF"/>
<stop offset="1" stop-color="#0094D4"/>
</linearGradient>
<linearGradient id="paint4_linear_125_2875" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
<stop stop-color="#22BCFF"/>
<stop offset="1" stop-color="#0088F0"/>
</linearGradient>
<linearGradient id="paint5_linear_125_2875" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
<stop stop-color="#28AFEA"/>
<stop offset="1" stop-color="#3CCBF4"/>
</linearGradient>
<clipPath id="clip0_125_2875">
<rect width="24" height="24" fill="white"/>
</clipPath>
<clipPath id="clip1_125_2875">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,97 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_125_2968)">
<path d="M3 1.5C3 0.671573 3.67157 0 4.5 0H19.5C20.3284 0 21 0.671573 21 1.5V16.5C21 17.3284 20.3284 18 19.5 18H4.5C3.67157 18 3 17.3284 3 16.5V1.5Z" fill="#9C640A"/>
<mask id="mask0_125_2968" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="0" width="18" height="18">
<path d="M3 1.5C3 0.671573 3.67157 0 4.5 0H19.5C20.3284 0 21 0.671573 21 1.5V16.5C21 17.3284 20.3284 18 19.5 18H4.5C3.67157 18 3 17.3284 3 16.5V1.5Z" fill="#9C640A"/>
</mask>
<g mask="url(#mask0_125_2968)">
<g filter="url(#filter0_dd_125_2968)">
<path d="M1.5 4.5C1.5 3.67157 2.17157 3 3 3H21C21.8284 3 22.5 3.67157 22.5 4.5V19.5C22.5 20.3284 21.8284 21 21 21H3C2.17157 21 1.5 20.3284 1.5 19.5V4.5Z" fill="#BC822A"/>
</g>
</g>
<path d="M1.5 4.5C1.5 3.67157 2.17157 3 3 3H21C21.8284 3 22.5 3.67157 22.5 4.5V19.5C22.5 20.3284 21.8284 21 21 21H3C2.17157 21 1.5 20.3284 1.5 19.5V4.5Z" fill="#BC822A"/>
<mask id="mask1_125_2968" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="1" y="3" width="22" height="18">
<path d="M1.5 4.5C1.5 3.67157 2.17157 3 3 3H21C21.8284 3 22.5 3.67157 22.5 4.5V19.5C22.5 20.3284 21.8284 21 21 21H3C2.17157 21 1.5 20.3284 1.5 19.5V4.5Z" fill="#BC822A"/>
</mask>
<g mask="url(#mask1_125_2968)">
<g filter="url(#filter1_dd_125_2968)">
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="#D9D9D9"/>
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint0_linear_125_2968)"/>
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint1_linear_125_2968)"/>
</g>
</g>
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="#D9D9D9"/>
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint2_linear_125_2968)"/>
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint3_linear_125_2968)"/>
<g filter="url(#filter2_dd_125_2968)">
<path d="M12 9C12.8284 9 13.5 9.67157 13.5 10.5V15.8789L14.6895 14.6895C15.2752 14.1037 16.2248 14.1037 16.8105 14.6895C17.3963 15.2752 17.3963 16.2248 16.8105 16.8105L13.0605 20.5605C12.4748 21.1463 11.5252 21.1463 10.9395 20.5605L7.18945 16.8105C6.60367 16.2248 6.60367 15.2752 7.18945 14.6895C7.77524 14.1037 8.72476 14.1037 9.31055 14.6895L10.5 15.8789V10.5C10.5 9.67157 11.1716 9 12 9Z" fill="url(#paint4_linear_125_2968)"/>
</g>
</g>
<defs>
<filter id="filter0_dd_125_2968" x="0.5" y="1.505" width="23" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-0.495"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2968"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-0.105"/>
<feGaussianBlur stdDeviation="0.05"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2968" result="effect2_dropShadow_125_2968"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2968" result="shape"/>
</filter>
<filter id="filter1_dd_125_2968" x="-1" y="4.505" width="26" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-0.495"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2968"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-0.105"/>
<feGaussianBlur stdDeviation="0.05"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2968" result="effect2_dropShadow_125_2968"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2968" result="shape"/>
</filter>
<filter id="filter2_dd_125_2968" x="5.75" y="8.5" width="12.5" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2968"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.1"/>
<feGaussianBlur stdDeviation="0.05"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_125_2968" result="effect2_dropShadow_125_2968"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2968" result="shape"/>
</filter>
<linearGradient id="paint0_linear_125_2968" x1="0" y1="6" x2="17.28" y2="29.04" gradientUnits="userSpaceOnUse">
<stop stop-color="#DEB678"/>
<stop offset="1" stop-color="#C59141"/>
</linearGradient>
<linearGradient id="paint1_linear_125_2968" x1="6.75" y1="3.75" x2="14.25" y2="26.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#DEB678"/>
<stop offset="1" stop-color="#B57F2D"/>
</linearGradient>
<linearGradient id="paint2_linear_125_2968" x1="0" y1="6" x2="17.28" y2="29.04" gradientUnits="userSpaceOnUse">
<stop stop-color="#DEB678"/>
<stop offset="1" stop-color="#C59141"/>
</linearGradient>
<linearGradient id="paint3_linear_125_2968" x1="6.75" y1="3.75" x2="14.25" y2="26.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#DEB678"/>
<stop offset="1" stop-color="#B57F2D"/>
</linearGradient>
<linearGradient id="paint4_linear_125_2968" x1="9.13631" y1="7.22729" x2="12.8104" y2="20.0865" gradientUnits="userSpaceOnUse">
<stop offset="0.270833" stop-color="white"/>
<stop offset="1" stop-color="#F0F0F0"/>
</linearGradient>
<clipPath id="clip0_125_2968">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -77,7 +77,7 @@
SelectedValue="{x:Bind ViewModel.CurrentFilter, Mode=OneWay}"
SelectionChanged="FiltersComboBox_SelectionChanged"
Style="{StaticResource ComboBoxStyle}"
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}, FallbackValue=Collapsed}">
<ComboBox.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultComboBoxItemStyle}" TargetType="ComboBoxItem">
<Setter Property="MinHeight" Value="0" />

View File

@@ -216,6 +216,16 @@ public sealed partial class SearchBar : UserControl,
e.Handled = true;
}
else if (e.Key == VirtualKey.PageDown)
{
WeakReferenceMessenger.Default.Send<NavigatePageDownCommand>();
e.Handled = true;
}
else if (e.Key == VirtualKey.PageUp)
{
WeakReferenceMessenger.Default.Send<NavigatePageUpCommand>();
e.Handled = true;
}
if (InSuggestion)
{

View File

@@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.System;
namespace Microsoft.CmdPal.UI;
@@ -25,6 +26,8 @@ namespace Microsoft.CmdPal.UI;
public sealed partial class ListPage : Page,
IRecipient<NavigateNextCommand>,
IRecipient<NavigatePreviousCommand>,
IRecipient<NavigatePageDownCommand>,
IRecipient<NavigatePageUpCommand>,
IRecipient<ActivateSelectedListItemMessage>,
IRecipient<ActivateSecondaryCommandMessage>
{
@@ -82,6 +85,8 @@ public sealed partial class ListPage : Page,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
@@ -94,6 +99,8 @@ public sealed partial class ListPage : Page,
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
@@ -181,9 +188,9 @@ public sealed partial class ListPage : Page,
var notificationText = li.Title;
UIHelper.AnnounceActionForAccessibility(
ItemsList,
notificationText,
"CommandPaletteSelectedItemChanged");
ItemsList,
notificationText,
"CommandPaletteSelectedItemChanged");
}
}
}
@@ -296,6 +303,142 @@ public sealed partial class ListPage : Page,
}
}
public void Receive(NavigatePageDownCommand message)
{
var indexes = CalculateTargetIndexPageUpDownScrollTo(true);
if (indexes is null)
{
return;
}
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
{
ItemView.SelectedIndex = indexes.Value.TargetIndex;
ItemView.ScrollIntoView(ItemView.SelectedItem);
}
}
public void Receive(NavigatePageUpCommand message)
{
var indexes = CalculateTargetIndexPageUpDownScrollTo(false);
if (indexes is null)
{
return;
}
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
{
ItemView.SelectedIndex = indexes.Value.TargetIndex;
ItemView.ScrollIntoView(ItemView.SelectedItem);
}
}
/// <summary>
/// Calculates the item index to target when performing a page up or page down
/// navigation. The calculation attempts to estimate how many items fit into
/// the visible viewport by measuring actual container heights currently visible
/// within the internal ScrollViewer. If measurements are not available a
/// fallback estimate is used.
/// </summary>
/// <param name="isPageDown">True to calculate a page-down target, false for page-up.</param>
/// <returns>
/// A tuple containing the current index and the calculated target index, or null
/// if a valid calculation could not be performed (for example, missing ScrollViewer).
/// </returns>
private (int CurrentIndex, int TargetIndex)? CalculateTargetIndexPageUpDownScrollTo(bool isPageDown)
{
var scroll = FindScrollViewer(ItemView);
if (scroll is null)
{
return null;
}
var viewportHeight = scroll.ViewportHeight;
if (viewportHeight <= 0)
{
return null;
}
var currentIndex = ItemView.SelectedIndex < 0 ? 0 : ItemView.SelectedIndex;
var itemCount = ItemView.Items.Count;
// Compute visible item heights within the ScrollViewer viewport
const int firstVisibleIndexNotFound = -1;
var firstVisibleIndex = firstVisibleIndexNotFound;
var visibleHeights = new List<double>(itemCount);
for (var i = 0; i < itemCount; i++)
{
if (ItemView.ContainerFromIndex(i) is FrameworkElement container)
{
try
{
var transform = container.TransformToVisual(scroll);
var topLeft = transform.TransformPoint(new Point(0, 0));
var bottom = topLeft.Y + container.ActualHeight;
// If any part of the container is inside the viewport, consider it visible
if (topLeft.Y >= 0 && bottom <= viewportHeight)
{
if (firstVisibleIndex == firstVisibleIndexNotFound)
{
firstVisibleIndex = i;
}
visibleHeights.Add(container.ActualHeight > 0 ? container.ActualHeight : 0);
}
}
catch
{
// ignore transform errors and continue
}
}
}
var itemsPerPage = 0;
// Calculate how many items fit in the viewport based on their actual heights
if (visibleHeights.Count > 0)
{
double accumulated = 0;
for (var i = 0; i < visibleHeights.Count; i++)
{
accumulated += visibleHeights[i] <= 0 ? 1 : visibleHeights[i];
itemsPerPage++;
if (accumulated >= viewportHeight)
{
break;
}
}
}
else
{
// fallback: estimate using first measured container height
double itemHeight = 0;
for (var i = currentIndex; i < itemCount; i++)
{
if (ItemView.ContainerFromIndex(i) is FrameworkElement { ActualHeight: > 0 } c)
{
itemHeight = c.ActualHeight;
break;
}
}
if (itemHeight <= 0)
{
itemHeight = 1;
}
itemsPerPage = Math.Max(1, (int)Math.Floor(viewportHeight / itemHeight));
}
var targetIndex = isPageDown
? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
: Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));
return (currentIndex, targetIndex);
}
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ListPage @this)
@@ -351,11 +494,11 @@ public sealed partial class ListPage : Page,
}
}
private ScrollViewer? FindScrollViewer(DependencyObject parent)
private static ScrollViewer? FindScrollViewer(DependencyObject parent)
{
if (parent is ScrollViewer)
if (parent is ScrollViewer viewer)
{
return (ScrollViewer)parent;
return viewer;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)

View File

@@ -22,7 +22,7 @@ internal static class WindowExtensions
appWindow.SetIcon(@"Assets\icon.ico");
}
private static HWND GetWindowHwnd(this Window window)
public static HWND GetWindowHwnd(this Window window)
{
return window is null
? throw new ArgumentNullException(nameof(window))

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.UI.Xaml;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.UI;
/// <summary>
/// Provides behavior to control taskbar and Alt+Tab presence by assigning a hidden owner
/// and toggling extended window styles for a target window.
/// </summary>
internal sealed class HiddenOwnerWindowBehavior
{
private HWND _hiddenOwnerHwnd;
private Window? _hiddenWindow;
/// <summary>
/// Shows or hides a window in the taskbar (and Alt+Tab) by updating ownership and extended window styles.
/// </summary>
/// <param name="target">The <see cref="Microsoft.UI.Xaml.Window"/> to update.</param>
/// <param name="isVisibleInTaskbar"> True to show the window in the taskbar (and Alt+Tab); false to hide it from both. </param>
/// <remarks>
/// When hiding the window, a hidden owner is assigned and <see cref="WINDOW_EX_STYLE.WS_EX_TOOLWINDOW"/>
/// is enabled to keep it out of the taskbar and Alt+Tab. When showing, the owner is cleared and
/// <see cref="WINDOW_EX_STYLE.WS_EX_APPWINDOW"/> is enabled to ensure taskbar presence. Since tool
/// windows use smaller corner radii, the normal rounded corners are enforced via
/// <see cref="DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND"/>.
/// </remarks>
/// <seealso href="https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons" />
public void ShowInTaskbar(Window target, bool isVisibleInTaskbar)
{
/*
* There are the three main ways to control whether a window appears on the taskbar:
* https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons
*
* 1. Set the window's owner. Owned windows do not appear on the taskbar:
* Turns out this is the most reliable way to hide a window from the taskbar and ALT+TAB. WinForms and WPF uses this method
* to back their ShowInTaskbar property as well.
*
* 2. Use the WS_EX_TOOLWINDOW extended window style:
* This mostly works, with some reports that it silently fails in some cases. The biggest issue
* is that for certain Windows settings (like Multitasking -> Show taskbar buttons on all displays = On all desktops),
* the taskbar button is always shown even for tool windows.
*
* 3. Using ITaskbarList:
* This is what AppWindow.IsShownInSwitchers uses, but it's COM-based and more complex, and can
* fail if Explorer isn't running or responding. It could be a good backup, if needed.
*/
var visibleHwnd = target.GetWindowHwnd();
if (isVisibleInTaskbar)
{
// remove any owner window
PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, HWND.Null);
}
else
{
// Set the hidden window as the owner of the target window
var hiddenHwnd = EnsureHiddenOwner();
PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, hiddenHwnd);
}
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
// Tool window and app window styles are mutually exclusive, change both just to be safe
target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !isVisibleInTaskbar);
target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_APPWINDOW, isVisibleInTaskbar);
// Since tool windows have smaller corner radii, we need to force the normal ones
target.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
}
private HWND EnsureHiddenOwner()
{
if (_hiddenOwnerHwnd.IsNull)
{
_hiddenWindow = new Window();
_hiddenOwnerHwnd = _hiddenWindow.GetWindowHwnd();
}
return _hiddenOwnerHwnd;
}
}

View File

@@ -57,14 +57,18 @@ public sealed partial class MainWindow : WindowEx,
private readonly List<TopLevelHotkey> _hotkeys = [];
private readonly KeyboardListener _keyboardListener;
private readonly LocalKeyboardListener _localKeyboardListener;
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
private bool _ignoreHotKeyWhenFullScreen = true;
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource;
private WindowPosition _currentWindowPosition = new();
public MainWindow()
{
InitializeComponent();
HideWindow();
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
@@ -73,6 +77,8 @@ public sealed partial class MainWindow : WindowEx,
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
}
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
_keyboardListener = new KeyboardListener();
_keyboardListener.Start();
@@ -80,7 +86,9 @@ public sealed partial class MainWindow : WindowEx,
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
PositionCentered();
RestoreWindowPosition();
UpdateWindowPositionInMemory();
SetAcrylic();
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
@@ -126,16 +134,6 @@ public sealed partial class MainWindow : WindowEx,
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
HideWindow();
ApplyWindowStyle();
}
private void ApplyWindowStyle()
{
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
// Since tool windows have smaller corner radii, we need to force the normal ones
this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !Debugger.IsAttached);
this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
@@ -161,6 +159,39 @@ public sealed partial class MainWindow : WindowEx,
PositionCentered(displayArea);
}
private void RestoreWindowPosition()
{
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.LastWindowPosition is not WindowPosition savedPosition)
{
PositionCentered();
return;
}
if (savedPosition.Width <= 0 || savedPosition.Height <= 0)
{
PositionCentered();
return;
}
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
var targetPoint = new PointInt32
{
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
};
AppWindow.Move(targetPoint);
}
private void PositionCentered(DisplayArea displayArea)
{
if (displayArea is not null)
@@ -175,6 +206,17 @@ public sealed partial class MainWindow : WindowEx,
}
}
private void UpdateWindowPositionInMemory()
{
_currentWindowPosition = new WindowPosition
{
X = AppWindow.Position.X,
Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height,
};
}
private void HotReloadSettings()
{
var settings = App.Current.Services.GetService<SettingsModel>()!;
@@ -257,14 +299,22 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
}
var display = GetScreen(hwnd, target);
PositionCentered(display);
if (target == MonitorBehavior.ToLast)
{
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
}
else
{
var display = GetScreen(hwnd, target);
PositionCentered(display);
}
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
// because that would make it hard to debug the app
if (Debugger.IsAttached)
{
ApplyWindowStyle();
_hiddenOwnerBehavior.ShowInTaskbar(this, true);
}
// Just to be sure, SHOW our hwnd.
@@ -419,6 +469,22 @@ public sealed partial class MainWindow : WindowEx,
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
var serviceProvider = App.Current.Services;
UpdateWindowPositionInMemory();
var settings = serviceProvider.GetService<SettingsModel>();
if (settings is not null)
{
settings.LastWindowPosition = new WindowPosition
{
X = _currentWindowPosition.X,
Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height,
};
SettingsModel.SaveSettings(settings);
}
var extensionService = serviceProvider.GetService<IExtensionService>()!;
extensionService.SignalStopExtensionsAsync();
@@ -490,6 +556,9 @@ public sealed partial class MainWindow : WindowEx,
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Save the current window position before hiding the window
UpdateWindowPositionInMemory();
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
{

View File

@@ -181,6 +181,15 @@
<Content Update="..\Microsoft.CmdPal.UI.ViewModels\Assets\template.zip">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\StoreLogo.dark.svg">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
<Content Update="Assets\StoreLogo.light.svg">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
<Content Update="Assets\WinGetLogo.svg">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View File

@@ -58,4 +58,9 @@ GetModuleHandle
GetWindowLong
SetWindowLong
WINDOW_EX_STYLE
WINDOW_EX_STYLE
CreateWindowEx
WNDCLASSEXW
RegisterClassEx
GetStockObject
GetModuleHandle

View File

@@ -48,7 +48,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
IRecipient<ShowConfirmationMessage>,
IRecipient<ShowToastMessage>,
IRecipient<NavigateToPageMessage>,
INotifyPropertyChanged
INotifyPropertyChanged,
IDisposable
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -65,6 +66,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private SettingsWindow? _settingsWindow;
private CancellationTokenSource? _focusAfterLoadedCts;
private WeakReference<Page>? _lastNavigatedPageRef;
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
public event PropertyChangedEventHandler? PropertyChanged;
@@ -447,7 +451,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
while (RootFrame.CanGoBack)
{
GoBack(withAnimation, focusSearch);
// don't focus on each step, just at the end
GoBack(withAnimation, focusSearch: false);
}
// focus search box, even if we were already home
if (focusSearch)
{
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
SearchBox.SelectSearch();
}
}
@@ -488,6 +500,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
if (e.Content is Page element)
{
_lastNavigatedPageRef = new WeakReference<Page>(element);
element.Loaded += FocusAfterLoaded;
}
}
@@ -497,6 +510,18 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
var page = (Page)sender;
page.Loaded -= FocusAfterLoaded;
// Only handle focus for the latest navigated page
if (_lastNavigatedPageRef is null || !_lastNavigatedPageRef.TryGetTarget(out var last) || !ReferenceEquals(page, last))
{
return;
}
// Cancel any previous pending focus work
_focusAfterLoadedCts?.Cancel();
_focusAfterLoadedCts?.Dispose();
_focusAfterLoadedCts = new CancellationTokenSource();
var token = _focusAfterLoadedCts.Token;
AnnounceNavigationToPage(page);
var shouldSearchBoxBeVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
@@ -509,34 +534,57 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
else
{
_ = Task.Run(async () =>
{
await page.DispatcherQueue.EnqueueAsync(async () =>
_ = Task.Run(
async () =>
{
// I hate this so much, but it can take a while for the page to be ready to accept focus;
// focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
for (var i = 0; i < 10; i++)
if (token.IsCancellationRequested)
{
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
{
var set = frameworkElement.Focus(FocusState.Programmatic);
if (set)
{
break;
}
}
await Task.Delay(100);
return;
}
// Update the search box visibility based on the current page:
// - We do this here after navigation so the focus is not jumping around too much,
// it messes with screen readers if we do it too early
// - Since this should hide the search box on content pages, it's not a problem if we
// wait for the code above to finish trying to focus the content
ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
});
});
try
{
await page.DispatcherQueue.EnqueueAsync(
async () =>
{
// I hate this so much, but it can take a while for the page to be ready to accept focus;
// focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
for (var i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
{
var set = frameworkElement.Focus(FocusState.Programmatic);
if (set)
{
break;
}
}
await Task.Delay(100, token);
}
token.ThrowIfCancellationRequested();
// Update the search box visibility based on the current page:
// - We do this here after navigation so the focus is not jumping around too much,
// it messes with screen readers if we do it too early
// - Since this should hide the search box on content pages, it's not a problem if we
// wait for the code above to finish trying to focus the content
ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
});
}
catch (OperationCanceledException)
{
// Swallow cancellation - another FocusAfterLoaded invocation superseded this one
}
catch (Exception ex)
{
Logger.LogError("Error during FocusAfterLoaded async focus work", ex);
}
},
token);
}
}
@@ -591,24 +639,31 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed;
if (e.Key == VirtualKey.Left && onlyAlt)
var onlyCtrl = !altPressed && ctrlPressed && !shiftPressed && !winPressed;
switch (e.Key)
{
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
e.Handled = true;
}
else if (e.Key == VirtualKey.Home && onlyAlt)
{
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
e.Handled = true;
}
else
{
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
case VirtualKey.Left when onlyAlt: // Alt+Left arrow
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
e.Handled = true;
break;
case VirtualKey.Home when onlyAlt: // Alt+Home
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
e.Handled = true;
break;
case (VirtualKey)188 when onlyCtrl: // Ctrl+,
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
e.Handled = true;
break;
default:
{
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
break;
}
}
}
@@ -658,4 +713,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
Logger.LogError("Error handling mouse button press event", ex);
}
}
public void Dispose()
{
_focusAfterLoadedCts?.Cancel();
_focusAfterLoadedCts?.Dispose();
_focusAfterLoadedCts = null;
}
}

View File

@@ -14,18 +14,204 @@
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<!-- ControlFillColorQuarternaryBrush does not exist (yet) in WinUI, only in Windows Visual Library (Figma) -->
<ResourceDictionary x:Key="Default">
<LinearGradientBrush x:Key="CardGradient2Brush" StartPoint="0,0" EndPoint="0.5, 1">
<GradientStop Offset="0" Color="#38C8AEC4" />
<GradientStop Offset="1" Color="#383286EE" />
</LinearGradientBrush>
<ImageSource x:Key="StoreLogo">ms-appx:///Assets/StoreLogo.dark.svg</ImageSource>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<LinearGradientBrush x:Key="CardGradient2Brush" StartPoint="0,0" EndPoint="1, 1">
<!--<GradientStop Offset="0" Color="#E6F0FC" />
<GradientStop Offset="0.4" Color="#E6F0FC" />
<GradientStop Offset="1" Color="#F0F0F7" />-->
<GradientStop Offset="0.0" Color="#FFF6F9FF" />
<!-- Light cool white -->
<GradientStop Offset="0.4" Color="#FFEFF5FF" />
<!-- Hinted lavender/blue -->
<GradientStop Offset="0.7" Color="#FFF7FAFD" />
<!-- Very soft neutral white -->
<GradientStop Offset="1.0" Color="#FFF5F8FA" />
<!-- Slight bluish-gray tint -->
<!-- Faint peach glow -->
<!-- Soft peach -->
</LinearGradientBrush>
<ImageSource x:Key="StoreLogo">ms-appx:///Assets/StoreLogo.light.svg</ImageSource>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="CardGradient2Brush" Color="Transparent" />
<SolidColorBrush x:Key="CardGradient1Brush" Color="Transparent" />
<ImageSource x:Key="StoreLogo">ms-appx:///Assets/StoreLogo.dark.svg</ImageSource>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<converters:BoolNegationConverter x:Key="InvertedBoolConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ResourceDictionary>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1">
<ScrollViewer x:Name="RootScrollViewer" Grid.Row="1">
<Grid Padding="16">
<StackPanel
MaxWidth="1000"
HorizontalAlignment="Stretch"
Spacing="{StaticResource SettingsCardSpacing}">
<ItemsRepeater ItemsSource="{x:Bind viewModel.CommandProviders, Mode=OneWay}" Layout="{StaticResource VerticalStackLayout}">
<!-- Banner -->
<Grid
Padding="16"
Background="{ThemeResource CardGradient2Brush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel x:Name="BannerDescriptionPanel" Orientation="Vertical">
<TextBlock x:Uid="Settings_ExtensionsPage_Banner_Header" FontWeight="SemiBold" />
<TextBlock
x:Uid="Settings_ExtensionsPage_Banner_Description"
Grid.Row="1"
MaxWidth="520"
HorizontalAlignment="Left"
TextWrapping="Wrap" />
<HyperlinkButton
x:Uid="Settings_ExtensionsPage_Banner_Hyperlink"
Padding="0"
Content="Learn how to create your own extensions"
NavigateUri="https://learn.microsoft.com/windows/powertoys/command-palette/overview" />
</StackPanel>
<StackPanel
x:Name="ButtonPanel"
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<Button x:Uid="Settings_ExtensionsPage_FindExtensions_MicrosoftStore" Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<Image AutomationProperties.AccessibilityView="Raw" Source="{ThemeResource StoreLogo}" />
</Viewbox>
<TextBlock Text="Microsoft Store" />
</StackPanel>
</Button>
<!--<Button x:Uid="Settings_ExtensionsPage_FindExtensions_WinGet" Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<Image AutomationProperties.AccessibilityView="Raw" Source="ms-appx:///Assets/WinGet.svg" />
</Viewbox>
<TextBlock Text="WinGet" />
</StackPanel>
</Button>-->
</StackPanel>
</Grid>
<!-- Search -->
<Grid Padding="0,24,0,8" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="320" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<AutoSuggestBox
x:Name="SearchBox"
x:Uid="Settings_ExtensionsPage_SearchBox_Placeholder"
HorizontalAlignment="Stretch"
Text="{x:Bind viewModel.Extensions.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<AutoSuggestBox.QueryIcon>
<SymbolIcon Symbol="Find" />
</AutoSuggestBox.QueryIcon>
<AutoSuggestBox.KeyboardAccelerators>
<KeyboardAccelerator
Key="F"
Invoked="OnFindInvoked"
Modifiers="Control" />
</AutoSuggestBox.KeyboardAccelerators>
</AutoSuggestBox>
<StackPanel
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<TextBlock
x:Name="ItemCounterTextBlock"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind viewModel.Extensions.ItemCounterText, Mode=OneWay}" />
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind viewModel.Extensions.ShowManualReloadOverlay, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressRing
Width="16"
Height="16"
IsActive="True" />
<TextBlock
x:Uid="Settings_ExtensionsPage_Reloading_Text"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}" />
</StackPanel>
<Button
x:Name="MoreButton"
x:Uid="Settings_ExtensionsPage_More_Button"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight">
<MenuFlyoutItem x:Uid="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem" Command="{x:Bind viewModel.Extensions.ReloadExtensionsCommand}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE777;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</Button.Flyout>
<FontIcon
AutomationProperties.AccessibilityView="Raw"
FontSize="16"
Glyph="&#xE712;" />
</Button>
</StackPanel>
</Grid>
<!-- Empty state when no results match the current search -->
<Grid
x:Name="NoResultsPanel"
Padding="48"
x:Load="{x:Bind viewModel.Extensions.ShowNoResultsPanel, Mode=OneWay}"
CornerRadius="4">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<TextBlock
x:Uid="Settings_ExtensionsPage_NoResults_Primary"
Style="{StaticResource BodyStrongTextBlockStyle}"
TextAlignment="Center" />
<TextBlock
x:Uid="Settings_ExtensionsPage_NoResults_Secondary"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextAlignment="Center" />
</StackPanel>
</Grid>
<ItemsRepeater
x:Name="ProvidersRepeater"
x:Load="{x:Bind viewModel.Extensions.HasResults, Mode=OneWay}"
ItemsSource="{x:Bind viewModel.Extensions.FilteredProviders, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
<controls:SettingsCard
@@ -54,5 +240,25 @@
</StackPanel>
</Grid>
</ScrollViewer>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LayoutVisualStates">
<VisualState x:Name="WideLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="NarrowLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ButtonPanel.(Grid.Row)" Value="1" />
<Setter Target="BannerDescriptionPanel.(Grid.ColumnSpan)" Value="2" />
<Setter Target="ButtonPanel.(Grid.Column)" Value="0" />
<Setter Target="ButtonPanel.Margin" Value="0,12,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

View File

@@ -8,6 +8,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Microsoft.CmdPal.UI.Settings;
@@ -35,4 +36,10 @@ public sealed partial class ExtensionsPage : Page
}
}
}
private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
SearchBox?.Focus(FocusState.Keyboard);
args.Handled = true;
}
}

View File

@@ -63,6 +63,7 @@
<ComboBoxItem x:Uid="Run_Radio_Position_Primary_Monitor" />
<ComboBoxItem x:Uid="Run_Radio_Position_Focus" />
<ComboBoxItem x:Uid="Run_Radio_Position_In_Place" />
<ComboBoxItem x:Uid="Run_Radio_Position_LastPosition" />
</ComboBox>
</controls:SettingsCard>

View File

@@ -25,7 +25,10 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="AppTitleBar" PaneToggleRequested="AppTitleBar_PaneToggleRequested">
<TitleBar
x:Name="AppTitleBar"
IsTabStop="False"
PaneToggleRequested="AppTitleBar_PaneToggleRequested">
<!-- 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

View File

@@ -428,7 +428,11 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="MoreCommandsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>More</value>
</data>
<data name="TrayMenu_Settings" xml:space="preserve">
<data name="Run_Radio_Position_LastPosition.Content" xml:space="preserve">
<value>Last Position</value>
<comment>Reopen the window where it was last closed</comment>
</data>
<data name="TrayMenu_Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="TrayMenu_Close" xml:space="preserve">
@@ -471,4 +475,46 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="ScreenReader_Announcement_NavigatedToPage0" xml:space="preserve">
<value>Navigated to {0} page</value>
</data>
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Settings (Ctrl+,)</value>
</data>
<data name="Settings_ExtensionsPage_NoResults_Primary.Text" xml:space="preserve">
<value>No extensions found</value>
</data>
<data name="Settings_ExtensionsPage_NoResults_Secondary.Text" xml:space="preserve">
<value>Try a different search term</value>
</data>
<data name="Settings_ExtensionsPage_More_Button.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>More options</value>
</data>
<data name="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem.Text" xml:space="preserve">
<value>Reload extensions</value>
</data>
<data name="Settings_ExtensionsPage_Reloading_Text.Text" xml:space="preserve">
<value>Reloading extensions..</value>
</data>
<data name="Settings_ExtensionsPage_Banner_Header.Text" xml:space="preserve">
<value>Discover more extensions</value>
</data>
<data name="Settings_ExtensionsPage_Banner_Description.Text" xml:space="preserve">
<value>Find more extensions on the Microsoft Store or WinGet.</value>
</data>
<data name="Settings_ExtensionsPage_Banner_Hyperlink.Content" xml:space="preserve">
<value>Learn how to create your own extensions</value>
</data>
<data name="Settings_ExtensionsPage_FindExtensions_MicrosoftStore.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Find extensions on the Microsoft Store</value>
</data>
<data name="Settings_ExtensionsPage_FindExtensions_MicrosoftStore.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Microsoft Store</value>
</data>
<data name="Settings_ExtensionsPage_FindExtensions_WinGet.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Find extensions on WinGet</value>
</data>
<data name="Settings_ExtensionsPage_FindExtensions_WinGet.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Microsoft Store</value>
</data>
<data name="Settings_ExtensionsPage_SearchBox_Placeholder.PlaceholderText" xml:space="preserve">
<value>Search extensions</value>
</data>
</root>

View File

@@ -25,11 +25,11 @@ public sealed partial class ToastWindow : WindowEx,
IRecipient<QuitMessage>
{
private readonly HWND _hwnd;
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new();
public ToastViewModel ViewModel { get; } = new();
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
public ToastWindow()
{
this.InitializeComponent();
@@ -39,12 +39,7 @@ public sealed partial class ToastWindow : WindowEx,
this.SetIcon();
AppWindow.Title = RS_.GetString("ToastWindowTitle");
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
// Since tool windows have smaller corner radii, we need to force the normal ones
// to visually match system toasts.
this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, true);
this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
_hiddenOwnerWindowBehavior.ShowInTaskbar(this, false);
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
PInvoke.EnableWindow(_hwnd, false);

View File

@@ -74,35 +74,11 @@
<ClCompile>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib /profile /opt:ref /opt:icf</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<ClCompile>
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib /profile /opt:ref /opt:icf</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">

View File

@@ -186,4 +186,48 @@ public class BookmarkManagerTests
Assert.AreEqual("D:\\UpdatedPath", updatedBookmark.Bookmark);
Assert.IsTrue(bookmarkUpdatedEventFired);
}
[TestMethod]
public void BookmarkManager_LegacyData_IdsArePersistedAcrossLoads()
{
// Arrange
const string json = """
{
"Data":
[
{ "Name": "C:\\","Bookmark": "C:\\" },
{ "Name": "Bing.com","Bookmark": "https://bing.com" }
]
}
""";
var dataSource = new MockBookmarkDataSource(json);
// First load: IDs should be generated for legacy entries
var manager1 = new BookmarksManager(dataSource);
var firstLoad = manager1.Bookmarks.ToList();
Assert.AreEqual(2, firstLoad.Count);
Assert.AreNotEqual(Guid.Empty, firstLoad[0].Id);
Assert.AreNotEqual(Guid.Empty, firstLoad[1].Id);
// Keep a name->id map to be insensitive to ordering
var firstIdsByName = firstLoad.ToDictionary(b => b.Name, b => b.Id);
// Wait deterministically for async persistence to complete
var wasSaved = dataSource.WaitForSave(1, 5000);
Assert.IsTrue(wasSaved, "Data was not saved within the expected time.");
// Second load: should read back the same IDs from persisted data
var manager2 = new BookmarksManager(dataSource);
var secondLoad = manager2.Bookmarks.ToList();
Assert.AreEqual(2, secondLoad.Count);
var secondIdsByName = secondLoad.ToDictionary(b => b.Name, b => b.Id);
foreach (var kvp in firstIdsByName)
{
Assert.IsTrue(secondIdsByName.ContainsKey(kvp.Key), $"Missing bookmark '{kvp.Key}' after reload.");
Assert.AreEqual(kvp.Value, secondIdsByName[kvp.Key], $"Bookmark '{kvp.Key}' upgraded ID was not persisted across loads.");
}
}
}

View File

@@ -110,7 +110,7 @@ public partial class BookmarkResolverTests
[
new PlaceholderClassificationCase(
Name: "Drive",
Input: "C:",
Input: "C:\\.",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: "C:\\",

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
@@ -9,6 +11,7 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
internal sealed class MockBookmarkDataSource : IBookmarkDataSource
{
private string _jsonData;
private int _saveCount;
public MockBookmarkDataSource(string initialJsonData = "[]")
{
@@ -23,5 +26,26 @@ internal sealed class MockBookmarkDataSource : IBookmarkDataSource
public void SaveBookmarkData(string jsonData)
{
_jsonData = jsonData;
Interlocked.Increment(ref _saveCount);
}
public int SaveCount => Volatile.Read(ref _saveCount);
// Waits until at least expectedMinSaves have occurred or the timeout elapses.
// Returns true if the condition was met, false on timeout.
public bool WaitForSave(int expectedMinSaves = 1, int timeoutMs = 2000)
{
var start = Environment.TickCount;
while (Volatile.Read(ref _saveCount) < expectedMinSaves)
{
if (Environment.TickCount - start > timeoutMs)
{
return false;
}
Thread.Sleep(50);
}
return true;
}
}

View File

@@ -103,7 +103,33 @@ internal sealed partial class BookmarksManager : IDisposable, IBookmarksManager
try
{
var jsonData = _dataSource.GetBookmarkData();
_bookmarksData = _parser.ParseBookmarks(jsonData);
var bookmarksData = _parser.ParseBookmarks(jsonData);
// Upgrade old bookmarks if necessary
// Pre .95 versions did not assign IDs to bookmarks
var upgraded = false;
for (var index = 0; index < bookmarksData.Data.Count; index++)
{
var bookmark = bookmarksData.Data[index];
if (bookmark.Id == Guid.Empty)
{
bookmarksData.Data[index] = bookmark with { Id = Guid.NewGuid() };
upgraded = true;
}
}
lock (_lock)
{
_bookmarksData = bookmarksData;
}
// LOAD BEARING: Save upgraded data back to file
// This ensures that old bookmarks are not repeatedly upgraded on each load,
// as the hotkeys and aliases are tied to the generated bookmark IDs.
if (upgraded)
{
_ = SaveChangesAsync();
}
}
catch (Exception ex)
{

View File

@@ -30,25 +30,72 @@ internal sealed partial class AddBookmarkForm : FormContent
"type": "Input.Text",
"style": "text",
"id": "bookmark",
"value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}},
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"value": {{EncodeString(url)}},
"label": {{EncodeString(Resources.bookmarks_form_bookmark_label)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
"errorMessage": {{EncodeString(Resources.bookmarks_form_bookmark_required)}},
"placeholder": {{EncodeString(Resources.bookmarks_form_bookmark_placeholder)}}
},
{
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
"value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": false,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
"label": {{EncodeString(Resources.bookmarks_form_name_label)}},
"value": {{EncodeString(name)}},
"isRequired": false
},
{
"type": "RichTextBlock",
"inlines": [
{
"type": "TextRun",
"text": {{EncodeString(Resources.bookmarks_form_hint_text1)}},
"isSubtle": true,
"size": "Small"
},
{
"type": "TextRun",
"text": " ",
"isSubtle": true,
"size": "Small"
},
{
"type": "TextRun",
"text": {{EncodeString(Resources.bookmarks_form_hint_text2)}},
"fontType": "Monospace",
"size": "Small"
},
{
"type": "TextRun",
"text": " ",
"isSubtle": true,
"size": "Small"
},
{
"type": "TextRun",
"text": {{EncodeString(Resources.bookmarks_form_hint_text3)}},
"isSubtle": true,
"size": "Small"
},
{
"type": "TextRun",
"text": " ",
"isSubtle": true,
"size": "Small"
},
{
"type": "TextRun",
"text": {{EncodeString(Resources.bookmarks_form_hint_text4)}},
"fontType": "Monospace",
"size": "Small"
}
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "{{Resources.bookmarks_form_save}}",
"title": {{EncodeString(Resources.bookmarks_form_save)}},
"data": {
"name": "name",
"bookmark": "bookmark"
@@ -59,6 +106,8 @@ internal sealed partial class AddBookmarkForm : FormContent
""";
}
private static string EncodeString(string s) => JsonSerializer.Serialize(s, BookmarkSerializationContext.Default.String);
public override CommandResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload);

View File

@@ -19,7 +19,7 @@ public sealed record BookmarkData
[SetsRequiredMembers]
public BookmarkData(Guid id, string? name, string? bookmark)
{
Id = id == Guid.Empty ? Guid.NewGuid() : id;
Id = id;
Name = name ?? string.Empty;
Bookmark = bookmark ?? string.Empty;
}

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -177,6 +177,15 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Enter URL or file path, you can use {placeholders}, e.g. https://www.bing.com/search?q={Query}.
/// </summary>
public static string bookmarks_form_bookmark_placeholder {
get {
return ResourceManager.GetString("bookmarks_form_bookmark_placeholder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to URL or file path is required.
/// </summary>
@@ -187,7 +196,44 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties {
}
/// <summary>
/// Looks up a localized string similar to Name.
/// Looks up a localized string similar to You can add placeholders to bookmarks, and Command Palette will prompt you to enter their values when you open the bookmark.
///A placeholder looks like this:.
/// </summary>
public static string bookmarks_form_hint_text1 {
get {
return ResourceManager.GetString("bookmarks_form_hint_text1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {placeholder}.
/// </summary>
public static string bookmarks_form_hint_text2 {
get {
return ResourceManager.GetString("bookmarks_form_hint_text2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to — for example:.
/// </summary>
public static string bookmarks_form_hint_text3 {
get {
return ResourceManager.GetString("bookmarks_form_hint_text3", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to https://www.bing.com/search?q={Query}.
/// </summary>
public static string bookmarks_form_hint_text4 {
get {
return ResourceManager.GetString("bookmarks_form_hint_text4", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name (optional).
/// </summary>
public static string bookmarks_form_name_label {
get {

View File

@@ -140,7 +140,7 @@
<comment>"Terminal" should be the localized name of the Windows Terminal</comment>
</data>
<data name="bookmarks_form_name_label" xml:space="preserve">
<value>Name</value>
<value>Name (optional)</value>
</data>
<data name="bookmarks_form_save" xml:space="preserve">
<value>Save</value>
@@ -185,4 +185,20 @@
<data name="bookmark_toast_failed_open_text" xml:space="preserve">
<value>Failed to open {0}</value>
</data>
<data name="bookmarks_form_hint_text1" xml:space="preserve">
<value>You can add placeholders to bookmarks, and Command Palette will prompt you to enter their values when you open the bookmark.
A placeholder looks like this:</value>
</data>
<data name="bookmarks_form_hint_text2" xml:space="preserve">
<value>{placeholder}</value>
</data>
<data name="bookmarks_form_hint_text3" xml:space="preserve">
<value>— for example:</value>
</data>
<data name="bookmarks_form_hint_text4" xml:space="preserve">
<value>https://www.bing.com/search?q={Query}</value>
</data>
<data name="bookmarks_form_bookmark_placeholder" xml:space="preserve">
<value>Enter URL or file path, you can use {placeholders}, e.g. https://www.bing.com/search?q={Query}</value>
</data>
</root>

View File

@@ -6,7 +6,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed class Icons
internal static class Icons
{
internal static IconInfo FileExplorerSegoeIcon { get; } = new("\uEC50");
@@ -19,4 +19,8 @@ internal sealed class Icons
internal static IconInfo DocumentIcon { get; } = new("\uE8A5"); // Document
internal static IconInfo FolderOpenIcon { get; } = new("\uE838"); // FolderOpen
internal static IconInfo FilesIcon { get; } = new("\uF571"); // PrintAllPages
internal static IconInfo FilterIcon { get; } = new("\uE71C"); // Filter
}

View File

@@ -0,0 +1,27 @@
// 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.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer;
internal sealed partial class SearchFilters : Filters
{
public SearchFilters()
{
CurrentFilterId = "all";
}
public override IFilterItem[] GetFilters()
{
return [
new Filter() { Id = "all", Name = Resources.Indexer_Filter_All, Icon = Icons.FilterIcon },
new Separator(),
new Filter() { Id = "folders", Name = Resources.Indexer_Filter_Folders_Only, Icon = Icons.FolderOpenIcon },
new Filter() { Id = "files", Name = Resources.Indexer_Filter_Files_Only, Icon = Icons.FilesIcon },
];
}
}

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -36,6 +37,11 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
PlaceholderText = Resources.Indexer_PlaceholderText;
_searchEngine = new();
_queryCookie = 10;
var filters = new SearchFilters();
filters.PropChanged += Filters_PropChanged;
Filters = filters;
CreateEmptyContent();
}
@@ -49,6 +55,11 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
initialQuery = query;
SearchText = query;
disposeSearchEngine = false;
var filters = new SearchFilters();
filters.PropChanged += Filters_PropChanged;
Filters = filters;
CreateEmptyContent();
}
@@ -79,30 +90,56 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
{
// {20D04FE0-3AEA-1069-A2D8-08002B30309D} is CLSID for "This PC"
const string template = "search-ms:query={0}&crumb=location:::{{20D04FE0-3AEA-1069-A2D8-08002B30309D}}";
var encodedSearchText = UrlEncoder.Default.Encode(SearchText);
var fullSearchText = FullSearchString(SearchText);
var encodedSearchText = UrlEncoder.Default.Encode(fullSearchText);
var command = string.Format(CultureInfo.CurrentCulture, template, encodedSearchText);
ShellHelpers.OpenInShell(command);
}
public override ICommandItem EmptyContent => _isEmptyQuery ? _noSearchEmptyContent : _nothingFoundEmptyContent;
private void Filters_PropChanged(object sender, IPropChangedEventArgs args)
{
PerformSearch(SearchText);
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (oldSearch != newSearch && newSearch != initialQuery)
{
_ = Task.Run(() =>
{
_isEmptyQuery = string.IsNullOrWhiteSpace(newSearch);
Query(newSearch);
LoadMore();
OnPropertyChanged(nameof(EmptyContent));
initialQuery = null;
});
PerformSearch(newSearch);
}
}
private void PerformSearch(string newSearch)
{
var actualSearch = FullSearchString(newSearch);
_ = Task.Run(() =>
{
_isEmptyQuery = string.IsNullOrWhiteSpace(actualSearch);
Query(actualSearch);
LoadMore();
OnPropertyChanged(nameof(EmptyContent));
initialQuery = null;
});
}
public override IListItem[] GetItems() => [.. _indexerListItems];
private string FullSearchString(string query)
{
switch (Filters.CurrentFilterId)
{
case "folders":
return $"{query} kind:folders";
case "files":
return $"{query} kind:NOT folders";
case "all":
default:
return query;
}
}
public override void LoadMore()
{
IsLoading = true;

View File

@@ -177,6 +177,33 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Files and folders.
/// </summary>
internal static string Indexer_Filter_All {
get {
return ResourceManager.GetString("Indexer_Filter_All", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Files.
/// </summary>
internal static string Indexer_Filter_Files_Only {
get {
return ResourceManager.GetString("Indexer_Filter_Files_Only", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Folders.
/// </summary>
internal static string Indexer_Filter_Folders_Only {
get {
return ResourceManager.GetString("Indexer_Filter_Folders_Only", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Find file from path.
/// </summary>

View File

@@ -196,4 +196,13 @@ You can try searching all files on this PC or adjust your indexing settings.</va
<data name="Indexer_Command_SearchAllFiles" xml:space="preserve">
<value>Search all files</value>
</data>
<data name="Indexer_Filter_All" xml:space="preserve">
<value>Files and folders</value>
</data>
<data name="Indexer_Filter_Folders_Only" xml:space="preserve">
<value>Folders</value>
</data>
<data name="Indexer_Filter_Files_Only" xml:space="preserve">
<value>Files</value>
</data>
</root>

View File

@@ -28,9 +28,10 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
public bool HasTag => !string.IsNullOrEmpty(_tag);
private readonly Lock _resultsLock = new();
private readonly Lock _taskLock = new();
private CancellationTokenSource? _cancellationTokenSource;
private Task<IEnumerable<CatalogPackage>>? _currentSearchTask;
private string? _nextSearchQuery;
private bool _isTaskRunning;
private List<CatalogPackage>? _results;
@@ -85,7 +86,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
stopwatch.Stop();
Logger.LogDebug($"Building ListItems took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(GetItems));
IsLoading = false;
return results;
}
}
@@ -98,8 +98,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
Properties.Resources.winget_no_packages_found,
};
IsLoading = false;
return [];
}
@@ -117,64 +115,70 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
private void DoUpdateSearchText(string newSearch)
{
// Cancel any ongoing search
if (_cancellationTokenSource is not null)
lock (_taskLock)
{
Logger.LogDebug("Cancelling old search", memberName: nameof(DoUpdateSearchText));
_cancellationTokenSource.Cancel();
}
_cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token;
IsLoading = true;
try
{
// Save the latest search task
_currentSearchTask = DoSearchAsync(newSearch, cancellationToken);
}
catch (OperationCanceledException)
{
// DO NOTHING HERE
return;
}
catch (Exception ex)
{
// Handle other exceptions
ExtensionHost.LogMessage($"[WinGet] DoUpdateSearchText throw exception: {ex.Message}");
return;
}
// Await the task to ensure only the latest one gets processed
_ = ProcessSearchResultsAsync(_currentSearchTask, newSearch);
}
private async Task ProcessSearchResultsAsync(
Task<IEnumerable<CatalogPackage>> searchTask,
string newSearch)
{
try
{
var results = await searchTask;
// Ensure this is still the latest task
if (_currentSearchTask == searchTask)
if (_isTaskRunning)
{
// Process the results (e.g., update UI)
UpdateWithResults(results, newSearch);
// If a task is running, queue the next search query
// Keep IsLoading = true since we still have work to do
Logger.LogDebug($"Task is running, queueing next search: '{newSearch}'", memberName: nameof(DoUpdateSearchText));
_nextSearchQuery = newSearch;
}
else
{
// No task is running, start a new search
Logger.LogDebug($"Starting new search: '{newSearch}'", memberName: nameof(DoUpdateSearchText));
_isTaskRunning = true;
_nextSearchQuery = null;
IsLoading = true;
_ = ExecuteSearchChainAsync(newSearch);
}
}
catch (OperationCanceledException)
}
private async Task ExecuteSearchChainAsync(string query)
{
while (true)
{
// Handle cancellation gracefully (e.g., log or ignore)
Logger.LogDebug($" Cancelled search for '{newSearch}'");
}
catch (Exception ex)
{
// Handle other exceptions
Logger.LogError("Unexpected error while processing results", ex);
try
{
Logger.LogDebug($"Executing search for '{query}'", memberName: nameof(ExecuteSearchChainAsync));
var results = await DoSearchAsync(query);
// Update UI with results
UpdateWithResults(results, query);
}
catch (Exception ex)
{
Logger.LogError($"Unexpected error while searching for '{query}'", ex);
}
// Check if there's a next query to process
string? nextQuery;
lock (_taskLock)
{
if (_nextSearchQuery is not null)
{
// There's a queued search, execute it
nextQuery = _nextSearchQuery;
_nextSearchQuery = null;
Logger.LogDebug($"Found queued search, continuing with: '{nextQuery}'", memberName: nameof(ExecuteSearchChainAsync));
}
else
{
// No more searches queued, mark task as completed
_isTaskRunning = false;
IsLoading = false;
Logger.LogDebug("No more queued searches, task chain completed", memberName: nameof(ExecuteSearchChainAsync));
break;
}
}
// Continue with the next query
query = nextQuery;
}
}
@@ -189,11 +193,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
RaiseItemsChanged();
}
private async Task<IEnumerable<CatalogPackage>> DoSearchAsync(string query, CancellationToken ct)
private async Task<IEnumerable<CatalogPackage>> DoSearchAsync(string query)
{
// Were we already canceled?
ct.ThrowIfCancellationRequested();
Stopwatch stopwatch = new();
stopwatch.Start();
@@ -230,9 +231,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
opts.Filters.Add(tagFilter);
}
// Clean up here, then...
ct.ThrowIfCancellationRequested();
var catalogTask = HasTag ? WinGetStatics.CompositeWingetCatalog : WinGetStatics.CompositeAllCatalog;
// Both these catalogs should have been instantiated by the
@@ -251,13 +249,11 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
findPackages_stopwatch.Start();
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
ct.ThrowIfCancellationRequested();
Logger.LogDebug($"Preface for \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
// BODGY, re: microsoft/winget-cli#5151
// FindPackagesAsync isn't actually async.
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts));
var searchResults = await internalSearchTask;
findPackages_stopwatch.Stop();
@@ -271,8 +267,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
return [];
}
ct.ThrowIfCancellationRequested();
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
// FYI Using .ToArray or any other kind of enumerable loop
@@ -282,8 +276,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
{
var match = searchResults.Matches[i];
ct.ThrowIfCancellationRequested();
var package = match.CatalogPackage;
results.Add(package);
}

View File

@@ -0,0 +1,29 @@
// 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;
namespace SamplePagesExtension.Pages.IssueSpecificPages;
internal sealed partial class AllIssueSamplesIndexPage : ListPage
{
public AllIssueSamplesIndexPage()
{
Icon = new IconInfo("🐛");
Name = "All Issue Samples Index Page";
}
public override IListItem[] GetItems()
{
return new IListItem[]
{
new ListItem(new SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage())
{
Title = "Issue 42827 - Filter Drop Down Stays Visible After Switching From List To Content Page",
Subtitle = "Repro steps: Open this page, open the filter dropdown, select a filter, navigate to a content page, navigate back to this page. The filter dropdown should be closed but it remains open.",
},
};
}
}

View File

@@ -0,0 +1,76 @@
// 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.Linq;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension.Pages.IssueSpecificPages;
internal sealed partial class SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage : DynamicListPage
{
public SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage()
{
Icon = new IconInfo(string.Empty);
Name = "Issue 42827 - Filters not hiding when navigating between pages";
IsLoading = true;
var filters = new SampleFilters();
filters.PropChanged += Filters_PropChanged;
Filters = filters;
}
private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged();
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged();
public override IListItem[] GetItems()
{
var items = SearchText.ToCharArray().Select(ch => new ListItem(new SampleContentPage()) { Title = ch.ToString() }).ToArray();
if (items.Length == 0)
{
items = [
new ListItem(new SampleContentPage()) { Title = "This List item will open a content page" },
new ListItem(new SampleContentPage()) { Title = "This List item will open a content page too" },
new ListItem(new SampleContentPage()) { Title = "Guess what this one will do?" },
];
}
if (!string.IsNullOrEmpty(Filters.CurrentFilterId))
{
switch (Filters.CurrentFilterId)
{
case "mod2":
items = items.Where((item, index) => (index + 1) % 2 == 0).ToArray();
break;
case "mod3":
items = items.Where((item, index) => (index + 1) % 3 == 0).ToArray();
break;
case "all":
default:
// No filtering
break;
}
}
foreach (var item in items)
{
item.Subtitle = "Filter drop-down should be hidden when navigating to a content page";
}
return items;
}
internal sealed partial class SampleFilters : Filters
{
public override IFilterItem[] GetFilters()
{
return
[
new Filter() { Id = "all", Name = "All" },
new Filter() { Id = "mod2", Name = "Every 2nd", Icon = new IconInfo("2") },
new Filter() { Id = "mod3", Name = "Every 3rd (and long name)", Icon = new IconInfo("3") },
];
}
}
}

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