Compare commits

...

54 Commits

Author SHA1 Message Date
Leilei Zhang
06c21166b9 fix ruler 2025-09-03 10:44:18 +08:00
Gleb Khmyznikov
40bfdbc7c0 Disable ScreenResolutionUtility for arm64 platform
Comment out the ScreenResolutionUtility task for non-arm64 platforms.
2025-08-29 18:58:43 +02:00
Gleb Khmyznikov
12c84b19c9 Merge branch 'main' into gleb/screen-ruler-uitests 2025-08-20 11:09:58 +02:00
leileizhang
759f5c02cb [Cmdpal] Use DynamicDependency to preserve trimmed Adaptive Card action types (#41027)
<!-- 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
1. Preserve Adaptive Card action types during trimming using
DynamicDependency
2. Revert PR https://github.com/microsoft/PowerToys/pull/41010

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-20 16:47:38 +08:00
Yu Leng
e0428eef1d [CmdPal] Add WinAppSDK dependency in SamplePageExtension And ProcessMonitorExtension (#41274)
<!-- 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
To be honest, I don't know why we need it. But without this dependency,
I can not deploy in my local env.

How to repro:
1. Pull main branch.
2. Git clean -xfd (clean up the output path)
3. Click deploy in the VS

<!-- 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-08-20 16:26:14 +08:00
Yu Leng
3bc746d0ff [CmdPal][UnitTests] Add/Migrate unit test for Apps and Bookmarks extension (#41238)
<!-- 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
1. Create Apps and Bookmarks ut project.
2. Refactor Apps and Bookmarks. And some interface in these extensions
to add a abstraction layer for testing purpose.
New interface list:
* ISettingsInterface
* IUWPApplication
* IAppCache
* IBookmarkDataSource
3. Add/Migrate some test case for Apps and Bookmarks extension

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

- [x] Closes: #41239 #41240
- [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
- [ ] **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>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-20 16:25:46 +08:00
Shawn Yuan
75526b9580 [Feature] PowerToys hotkey conflict detection (#41029)
<!-- 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
Implements comprehensive hotkey conflict detection and resolution system
for PowerToys, providing real-time conflict checking and centralized
management interface.

## 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
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **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)
- [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: [Shortcut conflict detction dev
spec](https://github.com/MicrosoftDocs/windows-dev-docs/pull/5519)

## TODO Lists
- [x] Add real-time hotkey validation functionality to the hotkey dialog
- [x] Immediately detect conflicts and update shortcut conflict status
after applying new shortcuts
- [x] Return conflict list from runner hotkey conflict detector for
conflict checking.
- [x] Implement the Tooltip for every shortcut control 
- [x] Add dialog UI for showing all the shortcut conflicts
- [x] Support changing shortcut directly inside the shortcut conflict
window/dialog, no need to nav to the settings page.
- [x] Redesign the `ShortcutConflictDialogContentControl` to align with
the spec
- [x] Add navigating and changing hotkey auctionability to the
`ShortcutConflictDialogContentControl`
- [x] Add telemetry. Impemented in [another
PR](https://github.com/shuaiyuanxx/PowerToys/pull/47)

## Shortcut Conflict Support Modules

![image](https://github.com/user-attachments/assets/3915174e-d1e7-4f86-8835-2a1bafcc85c9)

<details>
<summary>Demo videos</summary>


https://github.com/user-attachments/assets/476d992c-c6ca-4bcd-a3f2-b26cc612d1b9


https://github.com/user-attachments/assets/1c1a2537-de54-4db2-bdbf-6f1908ff1ce7


https://github.com/user-attachments/assets/9c992254-fc2b-402c-beec-20fceef25e6b


https://github.com/user-attachments/assets/d66abc1c-b8bf-45f8-a552-ec989dab310f
</details>

<!-- 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
Manually validation performed.

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-08-20 09:31:52 +08:00
Mike Griese
ce4d8dc11e CmdPal: Clean up ListItemViewModels when we no longer need them (#41169)
_We already fixed one leak, yes, but what about second leak?_

We already clean up `ListItemViewModel`s for a page when the page is
navigated away from. However, if the page updates it's items, we would
never actually `Cleanup` the old items. We'd just lose them, and never
unregister their event handlers. The objects would just leak forever.

This builds on the work in #41166, to do two things:
* Cleanup items that were removed from our list, when we actually update
`Items`. This involved a change to `Toolkit.ListHelpers`, to let us know
which items were removed from the list during `InPlaceUpdateList`
* Cleanup items that are thrown out when we cancel a FetchItems. Those
items were constructed, and might have registered event handlers, even
if we never actually put them into `Items`.

_Targets #41166_

Closes #39837

Tested with the evil sample from #41158, and loading thousands and
thousands of items no longer causes us to leak memory like we're
Deepwater Horizon.
2025-08-19 16:02:38 -05:00
rluengen
917da2e07e Remove all explicit dependencies from the toolkit and extensions api on WinAppSDK (#41261)
<!-- 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)?
-->
This pull request removes the dependencies from the toolkit and the SDK
on WinAppSDK and WebView2. This allows clients of these APIs to have
their own version dependencies.

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

- [ X] Closes: #41235 
- [ 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] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ X] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ X] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ X] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ 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

Co-authored-by: Ross Luengen <rossl@microsoft.com>
2025-08-19 15:53:41 -05:00
Niels Laute
ea6115f892 Remove debug dialog (#41250)
Quality of life improvement for the dev loop: removing the dialog in
Settings, since we have a DEBUG header in the titlebar anyways.
2025-08-19 15:51:33 +02:00
Mike Griese
8dbff245d6 CmdPal: cancel fetching new items when we get another one (#41166)
This just adds a simple `CancellationToken` around
`ListViewModel.FetchItems()`. Now, when we start a second `FetchItems`
(in responce to a `RaiseItemsChanged`), we'll cancel the old one first.
That'll prevent a particularly long first `GetItems` call from returning
after a second one has already set the list.

Closes #41149

No longer repros the evil sample from #41158
2025-08-19 06:20:06 -05:00
Jiří Polášek
fa741470bc CmdPal: Add local keyboard listener and use it to handle GoBack key (#41122)
## Summary of the Pull Request

Listener registers a hook on WH_KEYBOARD and raises an event when a key
is pressed down. Main window then uses it to handle the GoBack key that
we can't reach any other way.


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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-19 05:54:27 -05:00
Deniz
446d8087a3 Update readme.md (#41150)
<!-- 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 link to styling in developer documentation was not working. The path
was wrong.
<!-- 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
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-19 05:17:01 -05:00
Niels Laute
a0a8ce9f69 Adding Office / Copilot templates to KeyVisual (#41167)
Small change for #41161. For shortcuts in Settings, I guess we need to
figure out later how the Copilot/Office keys are mapped in our
shortcuts, but at least we have the right visual templates available.
2025-08-19 11:32:28 +02:00
Jiří Polášek
7b06fb3bdb CmdPal: Remove constrain that keeps the context menu flyout in the bounds of the window (#41133)
## Summary of the Pull Request

Added `ShouldConstrainToRootBounds="False"` to the Flyout element,
allowing it to extend beyond the bounds of its parent container. This
allows the menu to always open with top-left corner at the cursor
position as is common for the context menus.

This affects the menu only when opened as a context menu on the list
item (e.g. mouse right-click), not when opened from the Command Bar
(that opens same as before).

After screenshot:

<img width="834" height="589" alt="image"
src="https://github.com/user-attachments/assets/acb40e08-074e-4bae-afe7-87c6a73a6581"
/>


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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-19 14:32:16 +08:00
Mohammed Saalim K
6130d2ad39 Hosts: add “No leading spaces” option and honor it when saving (#41206)
## Summary of the Pull Request

Adds a new Hosts File Editor setting “No leading spaces” that prevents
prepending spaces to active lines when saving the hosts file (when any
entry is disabled). Default is Off to preserve current behavior.



## PR Checklist

- [x] Closes: #36386  

- [ ] Communication: N/A (small, scoped option)

- [x] Tests: Added/updated and all pass

- [x] Localization: New en-US strings added; other locales handled by
loc pipeline

- [ ] Dev docs: N/A

- [x] New binaries: None

- [x] Documentation updated: N/A



## Detailed Description of the Pull Request / Additional comments

- Settings surface:

  - `src/settings-ui/Settings.UI.Library/HostsProperties.cs`: add
`NoLeadingSpaces`

  - `src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs`: add
`NoLeadingSpaces`

  - `src/modules/Hosts/Hosts/Settings/UserSettings.cs`: load/save value
from settings.json

  - `src/settings-ui/Settings.UI/ViewModels/HostsViewModel.cs`: expose
`NoLeadingSpaces`

  - `src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml`: new
SettingsCard toggle

  - `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`: add
`Hosts_NoLeadingSpaces.Header/Description`

- Writer change:

  - `src/modules/Hosts/HostsUILib/Helpers/HostsService.cs`: gate indent
with `anyDisabled && !_userSettings.NoLeadingSpaces`

- Tests:

  - `src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs`:
`NoLeadingSpaces_Disabled_RemovesIndent`



Backward compatibility: default Off, current formatting unchanged unless
the user enables the option.



## Validation Steps Performed

- Automated: `HostsEditor.UnitTests` including
`NoLeadingSpaces_Disabled_RemovesIndent` passing.

- Manual:

  1. Run PowerToys (runner) as Admin.

  2. Settings → Hosts File Editor → enable “No leading spaces”.

  3. In editor, add active `127.0.0.10 example1` and disabled
`127.0.0.11 example2`; Save.

  4. Open `C:\Windows\System32\drivers\etc\hosts` in Notepad.

     - ON: active line starts at column 0; disabled is `# 127...`.

     - OFF: active line begins with two spaces when a disabled entry
exists.
2025-08-19 13:58:10 +08:00
Mike Griese
8737de29af CmdPal: mark CommandProvider.Dispose as virtual (#41184)
If your provider wants to implement this, they should be able to
2025-08-18 16:52:49 -05:00
Mike Griese
2f6876b85f CmdPal: Add a couple evil samples for testing (#41158)
This doesn't fix any bugs, it just makes them easier to repro

RE: #38190
RE: #41149
also accidentally a great example for RE: #39837
2025-08-18 16:46:36 -05:00
Jiří Polášek
8f93d0269f CmdPal: Honor "Single-click activation" only for pointer clicks and not for keyboard (#41119)
## Summary of the Pull Request

Changes the behavior of keyboard item activation when the item list view
has focus.
Previously, the list view handled item activation according to the
"Single-click activation" setting regardless of the input source (mouse,
pen, touch, or keyboard).

Now, when handling a ListView item click, the input source is detected,
and the "Single-click activation" setting is applied only for
pointer-raised clicks. For keyboard-triggered clicks, items are always
activated immediately.

Since the event `ListView.ItemClick` doesn't provide information about
what caused the item activation, this PR work around that by observing
last user input on the list immediately before `ItemClick` event is
invoked.

## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-18 16:46:08 -05:00
Jiří Polášek
d2a4c96e12 CmdPal: Prevent disposed ContentPage from handling messages (#41083)
## Summary of the Pull Request

Changes the timing of when `ContentPage` registers to messages from the
Toolkit Messenger so it happens only when navigated to, mirroring the
unregister on navigation from. Also unregisters from all messages when
unloaded.

Proactively unregisters the Settings window from all messages on close
instead of relying on the GC’s nondeterministic cleanup. Since the
Settings window is newly created each time, old instances can still
react to messages even after their time is over, merely waiting for GC
to collect them.

Co-authored-by: zadjii-msft <migrie@microsoft.com>


## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-18 11:45:25 -05:00
Jiří Polášek
409ae3d73a CmdPal: Improve page exception details for users (#41035)
## Summary of the Pull Request

Show timestamp, HRESULT (hex/decimal), and full Exception.ToString() in
the error message. Centralize message generation in a helper class for
consistency.

Example:

```
============================================================
😢 An unexpected error occurred in the 'Open' extension.

Summary:
  Message:    Operation is not valid due to the current state of the object. (inferred from HRESULT 0x80131509)
  Type:       System.Runtime.InteropServices.COMException
  Source:     WinRT.Runtime
  Time:       2025-08-07 15:54:20.4189499
  HRESULT:    0x80131509 (-2146233079)

Stack Trace:
   at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|38_0(Int32 hr)
   at ABI.Microsoft.CommandPalette.Extensions.IListPageMethods.GetItems(IObjectReference _obj)
   at Microsoft.CmdPal.Core.ViewModels.ListViewModel.FetchItems()
   at Microsoft.CmdPal.Core.ViewModels.ListViewModel.InitializeProperties()
   at Microsoft.CmdPal.Core.ViewModels.PageViewModel.InitializeAsync()

------------------ Full Exception Details ------------------
System.Runtime.InteropServices.COMException (0x80131509)
   at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|38_0(Int32 hr)
   at ABI.Microsoft.CommandPalette.Extensions.IListPageMethods.GetItems(IObjectReference _obj)
   at Microsoft.CmdPal.Core.ViewModels.ListViewModel.FetchItems()
   at Microsoft.CmdPal.Core.ViewModels.ListViewModel.InitializeProperties()
   at Microsoft.CmdPal.Core.ViewModels.PageViewModel.InitializeAsync()

ℹ️ If you need further assistance, please include this information in your support request.
ℹ️ Before sending, take a quick look to make sure it doesn't contain any personal or sensitive information.
============================================================

```

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

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

## Validation Steps Performed

I crashed an extension on purpose and read the message.
2025-08-18 11:31:41 -05:00
Heiko
65b752b3ff [CmdPal > Ext] Use empty content for WindowWalker, Windows Settings and Windows Search (#40722)
<!-- 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 improves the behavior of CmdPal on empty or wrong search query
for the following exts:
- Window Walker
- Windows Settings
- Windows Search (indexer)

### Window Walker
<img width="795" height="482" alt="image"
src="https://github.com/user-attachments/assets/352a122d-2b8f-45be-bf49-6a56f6ca0848"
/>

### Windows Settings - Empty query
<img width="796" height="485" alt="image"
src="https://github.com/user-attachments/assets/12f193b3-22c5-45d8-89c0-bba5740da62b"
/>

### Windows Settings - No search match
<img width="855" height="483" alt="image"
src="https://github.com/user-attachments/assets/e521f63d-65ae-4b93-992d-2bb0a11edaa7"
/>

### Windows search (indexer)
<img width="796" height="483" alt="image"
src="https://github.com/user-attachments/assets/c2e6a218-de2b-4657-a9e7-9def26c9258e"
/>



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

- [x] **Closes:** #40614 , #38293 , #40565
- [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
- [ ] **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: Mike Griese <migrie@microsoft.com>
2025-08-18 11:22:26 -05:00
Gleb Khmyznikov
501ff5690d minor addition to docs 2025-08-18 16:43:42 +02:00
Michael Jolley
6acb793184 CmdPal: Null pattern matching based on is expression rather than overridable operators (#40972)
What the title says. 😄 

Rather than relying on the potentially overloaded `!=` or `==` operators
when checking for null, now we'll use the `is` expression (possibly
combined with the `not` operator) to ensure correct checking. Probably
overkill for many of these classes, but decided to err on the side of
consistency. Would matter more on classes that may be inherited or
extended.

Using `is` and `is not` will provide us a guarantee that no
user-overloaded equality operators (`==`/`!=`) is invoked when a
`expression is null` is evaluated.

In code form, changed all instances of:

```c#
something != null

something == null
```

to:

```c#
something is not null

something is null
```

The one exception was checking null on a `KeyChord`. `KeyChord` is a
struct which is never null so VS will raise an error when trying this
versus just providing a warning when using `keyChord != null`. In
reality, we shouldn't do this check because it can't ever be null. In
the case of a `KeyChord` it **would** be a `KeyChord` equivalent to:

```c#
KeyChord keyChord = new ()
{
    Modifiers = 0,
    Vkey = 0,
    ScanCode = 0
};
```
2025-08-18 06:07:28 -05:00
Dustin L. Howett
efb48aa163 build: remove *tests* and all coverage/DIA DLLs from binskim (#41108)
This thing files about 900 bugs a month on us.

Before:

```
Done. 11,036 files scanned.
```

After:

```
Done. 4,753 files scanned.
```
2025-08-18 06:00:13 -05:00
leileizhang
e8754e4cd6 Fix: Move ImageResizer satellite resource dlls under WinUI3Apps<culture> (#41152)
<!-- 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
### Root cause:
Problem Previously the installer installed ImageResizer satellite
assemblies into [INSTALLFOLDER]<culture>*.dll. The runtime probes
WinUI3Apps<culture>\ for WinUI3 app resource assemblies, so localization
failed.

### Fix:
Updated Resources.wxs: ImageResizer_$(var.IdSafeLanguage)_Component now
targets
Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder".

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

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

## AI Summary
This pull request updates the installer configuration in `Resources.wxs`
to support resource management for WinUI 3 apps. The main changes ensure
that resource directories and uninstall logic properly handle the new
`WinUI3AppsInstallFolder`, and update the component registration for
localized resources.

**Installer resource management updates:**

* Added `WinUI3AppsInstallFolder` to the list of parent directories for
resource file generation, ensuring resources for WinUI 3 apps are
included during installer builds.

**Component and uninstall logic updates:**

* Updated the `ImageResizer` component to register its resources under
`Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder` instead of the
default install folder, aligning with the new directory structure for
WinUI 3 apps.
* Added uninstall logic to remove the localized resource folder for
`WinUI3AppsInstallFolder`, ensuring cleanup of WinUI 3 app resources
during uninstall.
2025-08-18 10:18:47 +08:00
Jiří Polášek
c4c9277f3f CmdPal: Fix regression when updating a command provider without commands (#40984)
Improves item insertion logic in TopLevelCommandManager.

Updated the insertion logic to handle invalid startIndex values. If
startIndex is -1, new items will be appended to the end of the
collection, enhancing robustness.

Fixes regression introduced in #40752

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

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-15 09:17:57 -05:00
Jiří Polášek
67cd0f055c CmdPal: Check icon parent before adding in ContentIcon (Closes: #40928) (#40931)
## Summary of the Pull Request

This pull request introduces a minor but important update to the
`ContentIcon` control in the `Microsoft.CmdPal.UI` module. The changes
improve robustness by adding checks to prevent duplicate parenting of
the `Content` element and include a debug assertion for better
diagnostics during development.

## PR Checklist

- [x] Closes: #40928 
- [ ] **Communication:** not yet
- [ ] **Tests:** nope
- [ ] **Localization:** none
- [ ] **Dev docs:** nay
- [ ] **New binaries:** no nothing
- [ ] **Documentation updated:** too lazy for that

## Detailed Description of the Pull Request / Additional comments

### Key changes:

#### Diagnostics and robustness improvements:
* Added a `Debug.Assert` statement to verify that the `Content` element
is not already parented to another element, helping to catch potential
issues during development.
(`[src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.csR39-R49](diffhunk://#diff-330aad69f925cf7a9e07bb7147af8e6cd09776a4c745455ac8a91a24b482d076R39-R49)`)
* Introduced checks to ensure the `Content` element is not added to the
`Grid`'s `Children` collection if it already exists there, preventing
redundant operations.
(`[src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.csR39-R49](diffhunk://#diff-330aad69f925cf7a9e07bb7147af8e6cd09776a4c745455ac8a91a24b482d076R39-R49)`)

#### Code maintenance:
* Added a `using System.Diagnostics` directive to enable the use of the
`Debug` class for assertions.
(`[src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.csR5](diffhunk://#diff-330aad69f925cf7a9e07bb7147af8e6cd09776a4c745455ac8a91a24b482d076R5)`)

## Validation Steps Performed

Turned extensions off and on and off and on and off and on and off and
on and off and on and off and on and off and on and off and on and off
and on and off and on and off and on and off and on and off and on and
off and on and off and on.
And then off and on again, just to be sure.

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-08-15 06:48:54 -05:00
Jiří Polášek
051c07885e CmdPal: Replace the brush used for the menu item separator (#41130)
## Summary of the Pull Request

Replace the brush used for the menu item separator in
SeparatorContextMenuViewModelTemplate with the brush used by WinUI 3 for
flyout menus.

The brush previously used is a legacy brush and a WinUI trap.

After screenshot:

<img width="830" height="513" alt="image"
src="https://github.com/user-attachments/assets/32620050-29a3-40a5-aa6a-fe83afb55090"
/>


## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-08-14 10:52:37 -05:00
Gleb Khmyznikov
38acf1a6bc disable some window attach methods 2025-08-11 18:45:50 +02:00
Gleb Khmyznikov
cb70e1dbfd Merge branch 'main' into gleb/screen-ruler-uitests 2025-08-11 10:16:54 +02:00
Gleb Khmyznikov
df750394db Filter EN symbols 2025-08-11 10:16:09 +02:00
Gleb Khmyznikov
9bcba8cb8b SendKey space 2025-08-07 18:36:28 +02:00
Gleb Khmyznikov
00457b2933 toggleSwitch to Toggle 2025-08-07 17:15:31 +02:00
Gleb Khmyznikov
2b0b80c63a add attempts 2025-08-07 16:29:09 +02:00
Gleb Khmyznikov
d42df8392c Trying to fix toggleClick 2025-08-07 15:35:01 +02:00
Gleb Khmyznikov
c6033bc8e5 change a bit the toggler click 2025-08-07 11:57:26 +02:00
Gleb Khmyznikov
69c206e85e increase more delays 2025-08-06 21:29:22 +02:00
Gleb Khmyznikov
0afb91b71d increase some delays 2025-08-06 19:52:59 +02:00
Gleb Khmyznikov
f926a5ece9 fix xaml format 2025-08-06 14:09:46 +02:00
Gleb Khmyznikov
b9e66cca26 Add big delays 2025-08-06 13:46:45 +02:00
Gleb Khmyznikov
c4dd213712 fix assembly name 2025-08-06 13:13:09 +02:00
Gleb Khmyznikov
2808f65d7f Merge branch 'main' into gleb/screen-ruler-uitests 2025-08-06 12:58:40 +02:00
Gleb Khmyznikov
0e61b28571 update solution name 2025-08-06 12:56:49 +02:00
Gleb Khmyznikov
ce406892b3 update the ruler test project name 2025-08-06 12:08:23 +02:00
Gleb Khmyznikov
f619334ff2 delete comment 2025-08-06 09:10:19 +02:00
Gleb Khmyznikov
142f09459d Add more tests and make the helper more straightforward 2025-08-05 19:43:16 +02:00
Gleb Khmyznikov
f42115bc4f Bounds tool test 2025-08-05 17:05:23 +02:00
Gleb Khmyznikov
36764487c5 Enabling screen ruler tests 2025-08-05 15:59:41 +02:00
Gleb Khmyznikov
218cb473c1 Basic complex test working 2025-08-04 18:19:04 +02:00
Gleb Khmyznikov
f0b674763a Use AutomationId instead of Name 2025-08-04 16:34:52 +02:00
Gleb Khmyznikov
cf0264e9b9 Add module toggle test 2025-08-04 12:06:18 +02:00
Gleb Khmyznikov
be7098c267 First simple test 2025-08-01 17:54:22 +02:00
Gleb Khmyznikov
3f486ea3db init 2025-08-01 10:39:36 +02:00
331 changed files with 8577 additions and 1109 deletions

View File

@@ -25,6 +25,8 @@ ADMINS
adml
admx
advancedpaste
advancedpasteui
advancedpasteuishortcut
advfirewall
AFeature
affordances
@@ -40,6 +42,7 @@ ALLINPUT
Allman
Allmodule
ALLOWUNDO
allpc
ALLVIEW
ALPHATYPE
AModifier
@@ -115,6 +118,7 @@ bigbar
bigobj
binlog
binres
binskim
BITMAPFILEHEADER
bitmapimage
BITMAPINFO
@@ -255,6 +259,7 @@ Corpor
cotaskmem
COULDNOT
countof
covrun
cpcontrols
cph
cplusplus
@@ -627,6 +632,7 @@ HKCU
hkey
HKLM
HKM
hkmng
HKPD
HKU
HMD
@@ -635,6 +641,7 @@ hmodule
hmonitor
homies
homljgmgpmcbpjbnjpfijnhipfkiclkd
HOOKPROC
HORZRES
HORZSIZE
Hostbackdropbrush
@@ -643,7 +650,11 @@ Hostx
hotfixes
hotkeycontrol
HOTKEYF
hotkeylockmachine
hotkeyreconnect
hotkeys
hotkeyswitch
hotkeytoggleeasymouse
hotlight
hotspot
HPAINTBUFFER
@@ -701,9 +712,12 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
imagetotext
imagetotextshortcut
imagingdevices
ime
imgflip
inapp
inbox
INCONTACT
Indo
@@ -757,6 +771,7 @@ istep
ith
ITHUMBNAIL
IUI
IUWP
IWIC
jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
@@ -786,6 +801,7 @@ keyvault
KILLFOCUS
killrunner
kmph
kvp
Kybd
lastcodeanalysissucceeded
LASTEXITCODE
@@ -824,6 +840,7 @@ localappdata
localpackage
LOCALSYSTEM
LOCATIONCHANGE
LOCKMACHINE
LOCKTYPE
LOGFONT
LOGFONTW
@@ -909,6 +926,7 @@ MDL
mdtext
mdtxt
mdwn
measuretool
meme
memicmp
MENUITEMINFO
@@ -958,6 +976,7 @@ MOUSEHWHEEL
MOUSEINPUT
mousejump
mousepointer
mousepointercrosshairs
mouseutils
MOVESIZEEND
MOVESIZESTART
@@ -969,6 +988,7 @@ msc
mscorlib
msctls
msdata
msdia
MSDL
MSGFLT
MSHCTX
@@ -1157,6 +1177,18 @@ PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEPARSING
parray
PARTIALCONFIRMATIONDIALOGTITLE
pasteashtmlfile
pasteashtmlfileshortcut
pasteasjson
pasteasjsonshortcut
pasteasmarkdown
pasteasmarkdownshortcut
pasteasplaintext
pasteasplaintextshortcut
pasteaspngfile
pasteaspngfileshortcut
pasteastxtfile
pasteastxtfileshortcut
PATCOPY
PATHMUSTEXIST
PATINVERT
@@ -1224,6 +1256,7 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
powerocr
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1364,6 +1397,7 @@ Removelnk
renamable
RENAMEONCOLLISION
reparented
reparenthotkey
reparenting
reportfileaccesses
requery
@@ -1613,6 +1647,7 @@ STYLECHANGED
STYLECHANGING
subkeys
sublang
Subdomain
SUBMODULEUPDATE
subresource
Superbar
@@ -1683,6 +1718,7 @@ THH
THICKFRAME
THISCOMPONENT
throughs
thumbnailhotkey
TILEDWINDOW
TILLSON
timedate
@@ -1697,6 +1733,7 @@ tlb
tlbimp
tlc
TNP
TOGGLEEASYMOUSE
Toolhelp
toolkitconverters
toolwindow
@@ -1710,6 +1747,7 @@ tracelogging
tracerpt
trackbar
trafficmanager
transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging

View File

@@ -64,6 +64,10 @@ extends:
tsa:
enabled: true
configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json'
binskim:
enabled: true
# Exclude every dll/exe in tests/*, as well as all msdia*, covrun* and vcruntime*
analyzeTargetGlob: +:file|$(Build.ArtifactStagingDirectory)/**/*.dll;+:file|$(Build.ArtifactStagingDirectory)/**/*.exe;-:file:regex|tests.*\.(dll|exe)$;-:file:regex|(covrun.*)\.dll$;-:file:regex|(msdia.*)\.dll$;-:file:regex|(vcruntime.*)\.dll$
stages:
- stage: Build

View File

@@ -147,10 +147,10 @@ jobs:
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "Machine"
displayName: Install PowerToys (Machine-Level)
- ${{ if ne(parameters.platform, 'arm64') }}:
- task: ScreenResolutionUtility@1
inputs:
displaySettings: 'optimal'
# - ${{ if ne(parameters.platform, 'arm64') }}:
# - task: ScreenResolutionUtility@1
# inputs:
# displaySettings: 'optimal'
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
- task: VSTest@3

View File

@@ -788,6 +788,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2850,6 +2858,22 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3161,6 +3185,8 @@ Global
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -71,6 +71,41 @@ When the user changes settings in the UI:
3. The runner calls the `set_config` function on the appropriate module
4. The module parses the JSON and applies the new settings
# Shortcut Conflict Detection
Steps to enable conflict detection for a hotkey:
### 1. Implement module interface for hotkeys
Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional<HotkeyEx> GetHotkeyEx()`.
- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
### 2. Implement IHotkeyConfig in the module settings (UI side)
Make sure the modules settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
- This method should return all hotkeys used in the module.
- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
### 3. Update the modules ViewModel
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()`.
- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
### 4. Ensure the modules Views call `OnPageLoaded()`
Once the modules view is loaded, make sure to invoke the ViewModels `OnPageLoaded()` method:
```cs
Loaded += (s, e) => ViewModel.OnPageLoaded();
```
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
## Debugging Settings
To debug settings issues:

View File

@@ -18,8 +18,8 @@ You can build the entire solution from the command line, which is sometimes fast
1. Open Developer Command Prompt for VS 2022
2. Navigate to the repository root directory
3. Run the following command(don't forget to set the correct platform):
```
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
```pwsh
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln /tl /p:NuGetInteractive="true"
```
4. This process should complete in approximately 13-14 minutes for a full build

View File

@@ -52,7 +52,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
## Rules
- **Follow the pattern of what you already see in the code.**
- [Coding style](development/style.md).
- [Coding style](style.md).
- Try to package new functionality/components into libraries that have nicely defined interfaces.
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.

View File

@@ -11,7 +11,7 @@
<Fragment>
<!-- Resource directories should be added only if the installer is built on the build farm -->
<?ifdef env.IsPipeline?>
<?foreach ParentDirectory in INSTALLFOLDER;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
<?foreach ParentDirectory in INSTALLFOLDER;WinUI3AppsInstallFolder;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
<DirectoryRef Id="$(var.ParentDirectory)">
<!-- Resource file directories -->
<?foreach Language in $(var.LocLanguageList)?>
@@ -181,7 +181,7 @@
</Component>
<Component
Id="ImageResizer_$(var.IdSafeLanguage)_Component"
Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER"
Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder"
Guid="$(var.CompGUIDPrefix)02">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="ImageResizer_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes"/>
@@ -553,6 +553,7 @@
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)HistoryPluginFolder" Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder" On="uninstall"/>
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)PowerToysPluginFolder" Directory="Resource$(var.IdSafeLanguage)PowerToysPluginFolder" On="uninstall"/>
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" Directory="Resource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" On="uninstall"/>
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" On="uninstall"/>
<?undef IdSafeLanguage?>
<?endforeach?>
</Component>

View File

@@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.UITest
Workspaces,
PowerRename,
CommandPalette,
ScreenRuler,
}
/// <summary>
@@ -104,6 +105,7 @@ namespace Microsoft.PowerToys.UITest
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
};
}

View File

@@ -112,7 +112,7 @@ private:
return {};
}
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
{
try
{
@@ -122,6 +122,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
hotkey.isShown = isShown;
return hotkey;
}
catch (...)
@@ -231,8 +232,10 @@ private:
return false;
}
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
{
bool actionIsShown = true;
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
return;
@@ -240,9 +243,9 @@ private:
const auto action = actionValue.GetObjectW();
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
{
return;
actionIsShown = false;
}
if (action.HasKey(JSON_KEY_SHORTCUT))
@@ -250,7 +253,7 @@ private:
const AdditionalAction additionalAction
{
actionName.c_str(),
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
};
m_additional_actions.push_back(additionalAction);
@@ -259,12 +262,12 @@ private:
{
for (const auto& [subActionName, subAction] : action)
{
process_additional_action(subActionName, subAction);
process_additional_action(subActionName, subAction, actionIsShown);
}
}
}
void read_settings(PowerToysSettings::PowerToyValues& settings)
void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@@ -317,9 +320,21 @@ private:
{
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
for (const auto& [actionName, additionalAction] : additionalActions)
// Define the expected order to ensure consistent hotkey ID assignment
const std::vector<winrt::hstring> expectedOrder = {
L"image-to-text",
L"paste-as-file",
L"transcode"
};
// Process actions in the predefined order
for (auto& actionKey : expectedOrder)
{
process_additional_action(actionName, additionalAction);
if (additionalActions.HasKey(actionKey))
{
const auto actionValue = additionalActions.GetNamedValue(actionKey);
process_additional_action(actionKey, actionValue);
}
}
}
@@ -331,17 +346,14 @@ private:
for (const auto& customAction : customActions)
{
const auto object = customAction.GetObjectW();
bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false);
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
{
const CustomAction customActionData
{
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
};
const CustomAction customActionData{
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
};
m_custom_actions.push_back(customActionData);
}
m_custom_actions.push_back(customActionData);
}
}
}

View File

@@ -298,5 +298,34 @@ namespace Hosts.Tests
var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden);
Assert.IsTrue(hidden);
}
[TestMethod]
public async Task NoLeadingSpaces_Disabled_RemovesIndent()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var expected =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
# 10.1.1.30 host30 host30.local # new entry
";
var fs = new CustomMockFileSystem();
var settings = new Mock<IUserSettings>();
settings.Setup(s => s.NoLeadingSpaces).Returns(true);
var svc = new HostsService(fs, settings.Object, _elevationHelper.Object);
fs.AddFile(svc.HostsFilePath, new MockFileData(content));
var data = await svc.ReadAsync();
var entries = data.Entries.ToList();
entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
await svc.WriteAsync(data.AdditionalLines, entries);
var result = fs.GetFile(svc.HostsFilePath);
Assert.AreEqual(expected, result.TextContents);
}
}
}

View File

@@ -26,6 +26,8 @@ namespace Hosts.Settings
private bool _loopbackDuplicates;
public bool NoLeadingSpaces { get; private set; }
public bool LoopbackDuplicates
{
get => _loopbackDuplicates;
@@ -88,6 +90,7 @@ namespace Hosts.Settings
AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition;
Encoding = (HostsEncoding)settings.Properties.Encoding;
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
NoLeadingSpaces = settings.Properties.NoLeadingSpaces;
}
retry = false;

View File

@@ -157,7 +157,7 @@ namespace HostsUILib.Helpers
{
lineBuilder.Append('#').Append(' ');
}
else if (anyDisabled)
else if (anyDisabled && !_userSettings.NoLeadingSpaces)
{
lineBuilder.Append(' ').Append(' ');
}

View File

@@ -19,5 +19,7 @@ namespace HostsUILib.Settings
event EventHandler LoopbackDuplicatesChanged;
public delegate void OpenSettingsFunction();
public bool NoLeadingSpaces { get; }
}
}

View File

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

View File

@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
Title="PowerToys.ScreenRuler"
IsAlwaysOnTop="True"
IsMaximizable="False"
IsMinimizable="False"
@@ -250,6 +251,7 @@
<ToggleButton
Name="btnBounds"
x:Uid="BtnBounds"
AutomationProperties.AutomationId="Button_Bounds"
Click="BoundsTool_Click"
Content="&#xEF20;"
KeyboardAcceleratorPlacementMode="Auto"
@@ -267,6 +269,7 @@
<ToggleButton
Name="btnSpacing"
x:Uid="BtnSpacing"
AutomationProperties.AutomationId="Button_Spacing"
Click="MeasureTool_Click"
Style="{StaticResource ToggleButtonRadioButtonStyle}">
<ToolTipService.ToolTip>
@@ -284,6 +287,7 @@
<ToggleButton
Name="btnHorizontalSpacing"
x:Uid="BtnHorizontalSpacing"
AutomationProperties.AutomationId="Button_SpacingHorizontal"
Click="HorizontalMeasureTool_Click"
Style="{StaticResource ToggleButtonRadioButtonStyle}">
<ToolTipService.ToolTip>
@@ -304,6 +308,7 @@
<ToggleButton
Name="btnVerticalSpacing"
x:Uid="BtnVerticalSpacing"
AutomationProperties.AutomationId="Button_SpacingVertical"
Click="VerticalMeasureTool_Click"
Style="{StaticResource ToggleButtonRadioButtonStyle}">
<ToolTipService.ToolTip>
@@ -324,6 +329,7 @@
<AppBarSeparator />
<Button
x:Uid="BtnClosePanel"
AutomationProperties.AutomationId="Button_Close"
Click="ClosePanelTool_Click"
Content="&#xE8BB;"
Foreground="{StaticResource CloseButtonBackgroundPointerOver}">

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="d4d0f157-5c12-4390-9689-152b0c86a582"
Publisher="CN=gkhmyznikov"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="d4d0f157-5c12-4390-9689-152b0c86a582" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>ScreenRuler.UITests</DisplayName>
<PublisherDisplayName>gkhmyznikov</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="ScreenRuler.UITests"
Description="ScreenRuler.UITests"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>PowerToys.ScreenRuler.UITests</RootNamespace>
<AssemblyName>ScreenRuler.UITests</AssemblyName>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\ScreenRuler.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>

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.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestBounds : UITestBase
{
public TestBounds()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.BoundsTool")]
[TestCategory("Spacing")]
public void TestScreenRulerBoundsTool()
{
TestHelper.InitializeTest(this, "bounds test");
TestHelper.PerformBoundsToolTest(this);
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,466 @@
// 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.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
public static class TestHelper
{
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
// Button automation names from Resources.resw
public const string BoundsButtonId = "Button_Bounds";
public const string SpacingButtonName = "Button_Spacing";
public const string HorizontalSpacingButtonName = "Button_SpacingHorizontal";
public const string VerticalSpacingButtonName = "Button_SpacingVertical";
public const string CloseButtonId = "Button_Close";
/// <summary>
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
/// </summary>
/// <param name="testBase">The test base instance</param>
/// <param name="testName">Name of the test for assertions</param>
/// <returns>The activation keys for the test</returns>
public static Key[] InitializeTest(UITestBase testBase, string testName)
{
LaunchFromSetting(testBase);
var toggleSwitch = SetScreenRulerToggle(testBase, enable: true);
Assert.IsTrue(
toggleSwitch.IsOn,
$"Screen Ruler toggle switch should be ON for {testName}");
var activationKeys = ReadActivationShortcut(testBase);
Assert.IsNotNull(activationKeys, "Should be able to read activation shortcut");
Assert.IsTrue(activationKeys.Length > 0, "Activation shortcut should contain at least one key");
return activationKeys;
}
/// <summary>
/// Performs common test cleanup: close ScreenRuler UI
/// </summary>
/// <param name="testBase">The test base instance</param>
public static void CleanupTest(UITestBase testBase)
{
CloseScreenRulerUI(testBase);
// Ensure we're attached to settings after cleanup
try
{
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
}
catch
{
// Ignore attachment errors - this is just cleanup
}
}
/// <summary>
/// Navigate to the Screen Ruler (Measure Tool) settings page
/// </summary>
public static void LaunchFromSetting(UITestBase testBase)
{
var screenRulers = testBase.Session.FindAll<NavigationViewItem>(By.AccessibilityId("Shell_Nav_ScreenRuler"));
if (screenRulers.Count == 0)
{
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("Shell_Nav_TopLevelSystemTools"), 5000).Click(msPostAction: 500);
}
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("Shell_Nav_ScreenRuler"), 5000).Click(msPostAction: 500);
}
/// <summary>
/// Set the Screen Ruler toggle switch to the specified state
/// </summary>
public static ToggleSwitch SetScreenRulerToggle(UITestBase testBase, bool enable)
{
var toggleSwitch = testBase.Session.Find<ToggleSwitch>(By.AccessibilityId("Toggle_ScreenRuler"), 5000);
if (toggleSwitch.IsOn != enable)
{
toggleSwitch.Click(msPreAction: 1000, msPostAction: 2000);
}
if (toggleSwitch.IsOn != enable)
{
testBase.Session.SendKey(Key.Space, msPreAction: 0, msPostAction: 2000);
}
return toggleSwitch;
}
/// <summary>
/// Set the Screen Ruler toggle and verify its state
/// </summary>
/// <param name="testBase">The test base instance</param>
/// <param name="enable">True to enable, false to disable</param>
/// <param name="testName">Name of the test for assertion messages</param>
public static void SetAndVerifyScreenRulerToggle(UITestBase testBase, bool enable, string testName)
{
var toggleSwitch = SetScreenRulerToggle(testBase, enable);
Assert.AreEqual(
enable,
toggleSwitch.IsOn,
$"Screen Ruler toggle switch should be {(enable ? "ON" : "OFF")} for {testName}");
}
/// <summary>
/// Read the current activation shortcut from the ShortcutControl
/// </summary>
public static Key[] ReadActivationShortcut(UITestBase testBase)
{
var shortcutCard = testBase.Session.Find<Element>(By.AccessibilityId("Shortcut_ScreenRuler"), 5000);
var shortcutButton = shortcutCard.Find<Element>(By.AccessibilityId("EditButton"), 5000);
return ParseShortcutText(shortcutButton.HelpText);
}
/// <summary>
/// Parse shortcut text like "Win + Ctrl + Shift + M" into Key array
/// </summary>
private static Key[] ParseShortcutText(string shortcutText)
{
if (string.IsNullOrEmpty(shortcutText))
{
return new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
}
var keys = new List<Key>();
var parts = shortcutText.Split(ShortcutSeparators, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var cleanPart = part.Trim().ToLowerInvariant();
var key = cleanPart switch
{
"win" or "windows" => Key.Win,
"ctrl" or "control" => Key.Ctrl,
"shift" => Key.Shift,
"alt" => Key.Alt,
_ when cleanPart.Length == 1 && char.IsLetter(cleanPart[0]) &&
cleanPart[0] >= 'a' && cleanPart[0] <= 'z' =>
(Key)Enum.Parse(typeof(Key), cleanPart.ToUpperInvariant()),
_ => (Key?)null,
};
if (key.HasValue)
{
keys.Add(key.Value);
}
}
return keys.Count > 0 ? keys.ToArray() : new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
}
/// <summary>
/// Check if ScreenRulerUI window is open
/// </summary>
public static bool IsScreenRulerUIOpen(UITestBase testBase) => testBase.IsWindowOpen("PowerToys.ScreenRuler");
/// <summary>
/// Wait for ScreenRulerUI to reach the specified state within the timeout
/// </summary>
public static bool WaitForScreenRulerUIState(UITestBase testBase, bool shouldBeOpen, int timeoutMs = 5000, int pollingIntervalMs = 100)
{
var endTime = DateTime.Now.AddMilliseconds(timeoutMs);
while (DateTime.Now < endTime)
{
if (IsScreenRulerUIOpen(testBase) == shouldBeOpen)
{
return true;
}
Task.Delay(pollingIntervalMs).Wait();
}
return false;
}
/// <summary>
/// Wait for ScreenRulerUI to appear within the specified timeout
/// </summary>
public static bool WaitForScreenRulerUI(UITestBase testBase, int timeoutMs = 5000) =>
WaitForScreenRulerUIState(testBase, shouldBeOpen: true, timeoutMs);
/// <summary>
/// Wait for ScreenRulerUI to disappear within the specified timeout
/// </summary>
public static bool WaitForScreenRulerUIToDisappear(UITestBase testBase, int timeoutMs = 5000) =>
WaitForScreenRulerUIState(testBase, shouldBeOpen: false, timeoutMs);
/// <summary>
/// Close ScreenRulerUI if it's open
/// </summary>
public static void CloseScreenRulerUI(UITestBase testBase)
{
if (IsScreenRulerUIOpen(testBase))
{
try
{
// Attach to ScreenRuler window before trying to find and click close button
testBase.Session.Attach(PowerToysModule.ScreenRuler);
var closeButton = testBase.Session.Find<Element>(By.AccessibilityId(CloseButtonId), 15000, true);
closeButton?.Click();
}
catch
{
// If we can't find the close button, ignore - the window might have closed already
}
finally
{
// Attach back to settings after closing
try
{
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
}
catch
{
// Ignore attachment errors
}
}
}
}
/// <summary>
/// Get a specific ScreenRulerUI button by its automation name
/// </summary>
public static Element? GetScreenRulerButton(UITestBase testBase, string buttonName, int timeoutMs = 1000)
{
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
/*
try
{
// Attach to ScreenRuler window before trying to find buttons
testBase.Session.Attach(PowerToysModule.ScreenRuler);
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
}
catch
{
return null;
}
finally
{
// Attach back to settings if needed for further operations
// This ensures we don't break the test flow
try
{
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
}
catch
{
// Ignore attachment errors - the calling code will handle as needed
}
}
*/
}
/// <summary>
/// Clear the clipboard content using STA thread
/// </summary>
public static void ClearClipboard()
{
ExecuteInSTAThread(() => System.Windows.Forms.Clipboard.Clear());
}
/// <summary>
/// Get text content from clipboard using STA thread
/// </summary>
public static string GetClipboardText()
{
string result = string.Empty;
ExecuteInSTAThread(() =>
{
if (System.Windows.Forms.Clipboard.ContainsText())
{
result = System.Windows.Forms.Clipboard.GetText();
}
});
return result ?? string.Empty;
}
/// <summary>
/// Execute an action in an STA thread with error handling
/// </summary>
private static void ExecuteInSTAThread(Action action)
{
try
{
var staThread = new Thread(() =>
{
try
{
action();
}
catch
{
// Ignore clipboard errors
}
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join(TimeSpan.FromSeconds(5));
}
catch
{
// Ignore clipboard errors
}
}
/// <summary>
/// Validate clipboard content contains valid spacing measurement for the specified type
/// </summary>
public static bool ValidateSpacingClipboardContent(string clipboardText, string spacingType)
{
if (string.IsNullOrEmpty(clipboardText))
{
return false;
}
return spacingType switch
{
"Spacing" => Regex.IsMatch(clipboardText, @"\d+\s*[<5B>x]\s*\d+"),
"Horizontal Spacing" or "Vertical Spacing" => Regex.IsMatch(clipboardText, @"^\d+$"),
_ => false,
};
}
/// <summary>
/// Perform a complete spacing tool test operation
/// </summary>
public static void PerformSpacingToolTest(UITestBase testBase, string buttonId, string testName)
{
ClearClipboard();
// Launch ScreenRuler UI
var activationKeys = ReadActivationShortcut(testBase);
testBase.SendKeys(activationKeys);
Assert.IsTrue(
WaitForScreenRulerUI(testBase, 2000),
$"ScreenRulerUI should appear after pressing activation shortcut for {testName}: {string.Join(" + ", activationKeys)}");
// Attach to ScreenRuler window and click spacing button
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
var spacingButton = testBase.Session.Find<Element>(By.AccessibilityId(buttonId), 15000, true);
Assert.IsNotNull(spacingButton, $"{testName} button should be found");
spacingButton!.Click();
Task.Delay(500).Wait();
// Perform measurement action (stay attached to ScreenRuler for this)
PerformMeasurementAction(testBase);
// Validate results
ValidateClipboardResults(testName);
// Cleanup - this will handle session attachment properly
CloseScreenRulerUI(testBase);
Assert.IsTrue(
WaitForScreenRulerUIToDisappear(testBase, 2000),
$"{testName}: ScreenRulerUI should close after calling CloseScreenRulerUI");
}
/// <summary>
/// Perform a bounds tool test operation
/// </summary>
public static void PerformBoundsToolTest(UITestBase testBase)
{
ClearClipboard();
var activationKeys = ReadActivationShortcut(testBase);
testBase.SendKeys(activationKeys);
Assert.IsTrue(
WaitForScreenRulerUI(testBase, 2000),
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
// Attach to ScreenRuler window and click bounds button
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
var boundsButton = testBase.Session.Find<Element>(By.AccessibilityId(BoundsButtonId), 15000, true);
Assert.IsNotNull(boundsButton, "Bounds button should be found");
boundsButton.Click();
Task.Delay(500).Wait();
// Perform drag operation to create 100x100 box (stay attached to ScreenRuler)
var currentPos = testBase.GetMousePosition();
int startX = currentPos.Item1;
int startY = currentPos.Item2 + 200;
testBase.MoveMouseTo(startX, startY);
Task.Delay(200).Wait();
// Drag operation
testBase.Session.PerformMouseAction(MouseActionType.LeftDown);
Task.Delay(100).Wait();
testBase.MoveMouseTo(startX + 99, startY + 99);
Task.Delay(200).Wait();
testBase.Session.PerformMouseAction(MouseActionType.LeftUp);
Task.Delay(500).Wait();
// Dismiss selection
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
Task.Delay(500).Wait();
// Validate results
string clipboardText = GetClipboardText();
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), "Clipboard should contain measurement data");
Assert.IsTrue(
clipboardText.Contains("100 <20> 100"),
$"Clipboard should contain '100 <20> 100', but contained: '{clipboardText}'");
// Cleanup - this will handle session attachment properly
CloseScreenRulerUI(testBase);
Assert.IsTrue(
WaitForScreenRulerUIToDisappear(testBase, 2000),
"ScreenRulerUI should close after calling CloseScreenRulerUI");
}
/// <summary>
/// Perform a measurement action (move mouse and click)
/// </summary>
private static void PerformMeasurementAction(UITestBase testBase)
{
var currentPos = testBase.GetMousePosition();
int startX = currentPos.Item1;
int startY = currentPos.Item2 + 200;
testBase.MoveMouseTo(startX, startY);
Task.Delay(200).Wait();
testBase.Session.PerformMouseAction(MouseActionType.LeftClick);
Task.Delay(500).Wait();
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
Task.Delay(500).Wait();
}
/// <summary>
/// Validate clipboard results for spacing tests
/// </summary>
private static void ValidateClipboardResults(string testName)
{
string clipboardText = GetClipboardText();
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), $"{testName}: Clipboard should contain measurement data");
bool containsValidPattern = ValidateSpacingClipboardContent(clipboardText, testName);
Assert.IsTrue(
containsValidPattern,
$"{testName}: Clipboard should contain valid spacing measurement, but contained: '{clipboardText}'");
}
}
}

View File

@@ -0,0 +1,72 @@
// 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.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestShortcutActivation : UITestBase
{
public TestShortcutActivation()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.ShortcutActivation")]
[TestCategory("Activation")]
public void TestScreenRulerShortcutActivation()
{
var activationKeys = TestHelper.InitializeTest(this, "activation test");
// Test 1: Press the activation shortcut and verify the toolbar appears
SendKeys(activationKeys);
bool screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
Assert.IsTrue(
screenRulerAppeared,
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
// Test 2: Press the activation shortcut again and verify the toolbar disappears
SendKeys(activationKeys);
bool screenRulerDisappeared = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
Assert.IsTrue(
screenRulerDisappeared,
$"ScreenRulerUI should disappear after pressing activation shortcut again: {string.Join(" + ", activationKeys)}");
// Test 3: Disable Screen Ruler and verify that the activation shortcut no longer activates the utility
// Ensure we're attached to settings UI before toggling
Session.Attach(PowerToysModule.PowerToysSettings);
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: false, "disabled state test");
// Try to activate with shortcut while disabled
SendKeys(activationKeys);
Task.Delay(1000).Wait();
Assert.IsFalse(
TestHelper.IsScreenRulerUIOpen(this),
"ScreenRulerUI should not appear when Screen Ruler is disabled");
// Test 4: Enable Screen Ruler and press the activation shortcut and verify the toolbar appears
// Ensure we're attached to settings UI before toggling
Session.Attach(PowerToysModule.PowerToysSettings);
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: true, "re-enabled state test");
SendKeys(activationKeys);
screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
Assert.IsTrue(
screenRulerAppeared,
$"ScreenRulerUI should appear after re-enabling and pressing activation shortcut: {string.Join(" + ", activationKeys)}");
// Test 5: Verify the utility can be closed via the cleanup method
TestHelper.CloseScreenRulerUI(this);
bool screenRulerClosed = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
Assert.IsTrue(
screenRulerClosed,
"ScreenRulerUI should close after calling CloseScreenRulerUI");
TestHelper.CleanupTest(this);
}
}
}

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.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestSpacing : UITestBase
{
public TestSpacing()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.SpacingTool")]
[TestCategory("Spacing")]
public void TestScreenRulerSpacingTool()
{
TestHelper.InitializeTest(this, "spacing test");
TestHelper.PerformSpacingToolTest(this, TestHelper.SpacingButtonName, "Spacing");
TestHelper.CleanupTest(this);
}
}
}

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.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestSpacingHorizontal : UITestBase
{
public TestSpacingHorizontal()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.HorizontalSpacingTool")]
[TestCategory("Spacing")]
public void TestScreenRulerHorizontalSpacingTool()
{
TestHelper.InitializeTest(this, "horizontal spacing test");
TestHelper.PerformSpacingToolTest(this, TestHelper.HorizontalSpacingButtonName, "Horizontal Spacing");
TestHelper.CleanupTest(this);
}
}
}

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.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestSpacingVertical : UITestBase
{
public TestSpacingVertical()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.VerticalSpacingTool")]
[TestCategory("Spacing")]
public void TestScreenRulerVerticalSpacingTool()
{
TestHelper.InitializeTest(this, "vertical spacing test");
TestHelper.PerformSpacingToolTest(this, TestHelper.VerticalSpacingButtonName, "Vertical Spacing");
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ScreenRuler.UITests.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -556,6 +556,61 @@ public:
return m_enabled;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
if (hotkeys && buffer_size >= num_hotkeys)
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
// Cache the raw JSON object to avoid multiple parsing
json::JsonObject root_json = values.get_raw_json();
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
size_t hotkey_index = 0;
// Helper lambda to extract hotkey from JSON properties
auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
if (properties_json.HasKey(property_name))
{
try
{
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
// Extract hotkey properties directly from JSON
bool win = hotkey_json.GetNamedBoolean(L"win", false);
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
unsigned char key = static_cast<unsigned char>(
hotkey_json.GetNamedNumber(L"code", 0));
return { win, ctrl, shift, alt, key };
}
catch (...)
{
// If parsing individual hotkey fails, use defaults
return { false, false, false, false, 0 };
}
}
else
{
// Property doesn't exist, use defaults
return { false, false, false, false, 0 };
}
};
// Extract all hotkeys using the optimized helper
hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
}
return num_hotkeys;
}
void launch_add_firewall_process()
{
Logger::trace(L"Starting Process to add firewall rule");

View File

@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Provides utility methods for building diagnostic and error messages.
/// </summary>
public static class DiagnosticsHelper
{
/// <summary>
/// Builds a comprehensive exception message with timestamp and detailed diagnostic information.
/// </summary>
/// <param name="exception">The exception that occurred.</param>
/// <param name="extensionHint">A hint about which extension caused the exception to help with debugging.</param>
/// <returns>A string containing the exception details, timestamp, and source information for diagnostic purposes.</returns>
public static string BuildExceptionMessage(Exception exception, string? extensionHint)
{
var locationHint = string.IsNullOrWhiteSpace(extensionHint) ? "application" : $"'{extensionHint}' extension";
// let's try to get a message from the exception or inferred it from the HRESULT
// to show at least something
var message = exception.Message;
if (string.IsNullOrWhiteSpace(message))
{
var temp = Marshal.GetExceptionForHR(exception.HResult)?.Message;
if (!string.IsNullOrWhiteSpace(temp))
{
message = temp + $" (inferred from HRESULT 0x{exception.HResult:X8})";
}
}
if (string.IsNullOrWhiteSpace(message))
{
message = "[No message available]";
}
// note: keep date time kind and format consistent with the log
return $"""
============================================================
😢 An unexpected error occurred in the {locationHint}.
Summary:
Message: {message}
Type: {exception.GetType().FullName}
Source: {exception.Source ?? "N/A"}
Time: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fffffff}
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
Stack Trace:
{exception.StackTrace ?? "[No stack trace available]"}
------------------ Full Exception Details ------------------
{exception}
If you need further assistance, please include this information in your support request.
Before sending, take a quick look to make sure it doesn't contain any personal or sensitive information.
============================================================
""";
}
}

View File

@@ -24,7 +24,7 @@ public partial class ExtensionHostInstance
/// <param name="message">The log message to send</param>
public void LogMessage(ILogMessage message)
{
if (Host != null)
if (Host is not null)
{
_ = Task.Run(async () =>
{
@@ -47,7 +47,7 @@ public partial class ExtensionHostInstance
public void ShowStatus(IStatusMessage message, StatusContext context)
{
if (Host != null)
if (Host is not null)
{
_ = Task.Run(async () =>
{
@@ -64,7 +64,7 @@ public partial class ExtensionHostInstance
public void HideStatus(IStatusMessage message)
{
if (Host != null)
if (Host is not null)
{
_ = Task.Run(async () =>
{

View File

@@ -36,7 +36,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
public IAsyncAction HideStatus(IStatusMessage? message)
{
if (message == null)
if (message is null)
{
return Task.CompletedTask.AsAsyncAction();
}
@@ -55,7 +55,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
public IAsyncAction LogMessage(ILogMessage? message)
{
if (message == null)
if (message is null)
{
return Task.CompletedTask.AsAsyncAction();
}
@@ -80,7 +80,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
try
{
var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
if (vm != null)
if (vm is not null)
{
StatusMessages.Remove(vm);
}
@@ -113,7 +113,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
{
// If this message is already in the list of messages, just bring it to the top
var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
if (oldVm != null)
if (oldVm is not null)
{
Task.Factory.StartNew(
() =>
@@ -142,7 +142,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context)
{
if (message == null)
if (message is null)
{
return Task.CompletedTask.AsAsyncAction();
}

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
@@ -35,13 +34,13 @@ public partial class CommandBarViewModel : ObservableObject,
[NotifyPropertyChangedFor(nameof(HasPrimaryCommand))]
public partial CommandItemViewModel? PrimaryCommand { get; set; }
public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible;
public bool HasPrimaryCommand => PrimaryCommand is not null && PrimaryCommand.ShouldBeVisible;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasSecondaryCommand))]
public partial CommandItemViewModel? SecondaryCommand { get; set; }
public bool HasSecondaryCommand => SecondaryCommand != null;
public bool HasSecondaryCommand => SecondaryCommand is not null;
[ObservableProperty]
public partial bool ShouldShowContextMenu { get; set; } = false;
@@ -58,14 +57,14 @@ public partial class CommandBarViewModel : ObservableObject,
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
if (value is not null)
{
PrimaryCommand = value.PrimaryCommand;
value.PropertyChanged += SelectedItemPropertyChanged;
}
else
{
if (SelectedItem != null)
if (SelectedItem is not null)
{
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
@@ -88,7 +87,7 @@ public partial class CommandBarViewModel : ObservableObject,
private void UpdateContextItems()
{
if (SelectedItem == null)
if (SelectedItem is null)
{
SecondaryCommand = null;
ShouldShowContextMenu = false;
@@ -127,13 +126,13 @@ public partial class CommandBarViewModel : ObservableObject,
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = SelectedItem?.Keybindings();
if (keybindings != null)
if (keybindings is not null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem))
{
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
return matchedItem is not null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
}
}
@@ -142,7 +141,7 @@ public partial class CommandBarViewModel : ObservableObject,
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
{
if (command == null)
if (command is null)
{
return ContextKeybindingResult.Unhandled;
}

View File

@@ -20,7 +20,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
public KeyChord? RequestedShortcut { get; private set; }
public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord);
public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord);
public override void InitializeProperties()
{
@@ -32,7 +32,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
base.InitializeProperties();
var contextItem = Model.Unsafe;
if (contextItem == null)
if (contextItem is null)
{
return; // throw?
}

View File

@@ -68,7 +68,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{
get
{
List<IContextItemViewModel> l = _defaultCommandContextItem == null ?
List<IContextItemViewModel> l = _defaultCommandContextItem is null ?
new() :
[_defaultCommandContextItem];
@@ -100,7 +100,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
var model = _commandItemModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
@@ -128,7 +128,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
var model = _commandItemModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
@@ -136,7 +136,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Command.InitializeProperties();
var listIcon = model.Icon;
if (listIcon != null)
if (listIcon is not null)
{
_listItemIcon = new(listIcon);
_listItemIcon.InitializeProperties();
@@ -172,13 +172,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
var model = _commandItemModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
var more = model.MoreCommands;
if (more != null)
if (more is not null)
{
MoreCommands = more
.Select(item =>
@@ -300,7 +300,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
protected virtual void FetchProperty(string propertyName)
{
var model = this._commandItemModel.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -308,7 +308,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
switch (propertyName)
{
case nameof(Command):
if (Command != null)
if (Command is not null)
{
Command.PropertyChanged -= Command_PropertyChanged;
}
@@ -339,7 +339,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
case nameof(model.MoreCommands):
var more = model.MoreCommands;
if (more != null)
if (more is not null)
{
var newContextMenu = more
.Select(item =>
@@ -394,7 +394,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// 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.
var model = _commandItemModel.Unsafe;
if (model != null)
if (model is not null)
{
_itemTitle = model.Title;
}
@@ -430,7 +430,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Command.SafeCleanup();
var model = _commandItemModel.Unsafe;
if (model != null)
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
}

View File

@@ -44,7 +44,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
}
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return;
}
@@ -67,13 +67,13 @@ public partial class CommandViewModel : ExtensionObjectViewModel
}
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return;
}
var ico = model.Icon;
if (ico != null)
if (ico is not null)
{
Icon = new(ico);
Icon.InitializeProperties();
@@ -98,7 +98,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
protected void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -125,7 +125,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
Icon = new(null); // necessary?
var model = Model.Unsafe;
if (model != null)
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
}

View File

@@ -25,7 +25,7 @@ public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReferen
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -28,7 +28,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
public DetailsViewModel? Details { get; private set; }
[MemberNotNullWhen(true, nameof(Details))]
public bool HasDetails => Details != null;
public bool HasDetails => Details is not null;
/////// ICommandBarContext ///////
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
@@ -67,7 +67,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
foreach (var item in newItems)
{
var viewModel = ViewModelFromContent(item, PageContext);
if (viewModel != null)
if (viewModel is not null)
{
viewModel.InitializeProperties();
newContent.Add(viewModel);
@@ -104,7 +104,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
base.InitializeProperties();
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -133,7 +133,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
});
var extensionDetails = model.Details;
if (extensionDetails != null)
if (extensionDetails is not null)
{
Details = new(extensionDetails, PageContext);
Details.InitializeProperties();
@@ -156,7 +156,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
base.FetchProperty(propertyName);
var model = this._model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -166,7 +166,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
case nameof(Commands):
var more = model.Commands;
if (more != null)
if (more is not null)
{
var newContextMenu = more
.ToList()
@@ -216,7 +216,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
UpdateDetails();
break;
}
@@ -248,7 +248,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
[RelayCommand]
private void InvokePrimaryCommand(ContentPageViewModel page)
{
if (PrimaryCommand != null)
if (PrimaryCommand is not null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
@@ -258,7 +258,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
[RelayCommand]
private void InvokeSecondaryCommand(ContentPageViewModel page)
{
if (SecondaryCommand != null)
if (SecondaryCommand is not null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
}
@@ -285,7 +285,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
Content.Clear();
var model = _model.Unsafe;
if (model != null)
if (model is not null)
{
model.ItemsChanged -= Model_ItemsChanged;
}

View File

@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Diagnostics.Utilities;
using Windows.System;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -51,7 +50,7 @@ public partial class ContextMenuViewModel : ObservableObject,
public void UpdateContextItems()
{
if (SelectedItem != null)
if (SelectedItem is not null)
{
if (SelectedItem.MoreCommands.Count() > 1)
{
@@ -68,14 +67,14 @@ public partial class ContextMenuViewModel : ObservableObject,
return;
}
if (SelectedItem == null)
if (SelectedItem is null)
{
return;
}
_lastSearchText = searchText;
if (CurrentContextMenu == null)
if (CurrentContextMenu is null)
{
ListHelpers.InPlaceUpdateList(FilteredItems, []);
return;
@@ -124,7 +123,7 @@ public partial class ContextMenuViewModel : ObservableObject,
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
if (CurrentContextMenu == null)
if (CurrentContextMenu is null)
{
return [];
}
@@ -140,7 +139,7 @@ public partial class ContextMenuViewModel : ObservableObject,
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = Keybindings();
if (keybindings != null)
if (keybindings is not null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
@@ -190,7 +189,7 @@ public partial class ContextMenuViewModel : ObservableObject,
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
if (CurrentContextMenu != null)
if (CurrentContextMenu is not null)
{
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
@@ -198,7 +197,7 @@ public partial class ContextMenuViewModel : ObservableObject,
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
{
if (command == null)
if (command is null)
{
return ContextKeybindingResult.Unhandled;
}

View File

@@ -22,7 +22,7 @@ public partial class DetailsCommandsViewModel(
{
base.InitializeProperties();
var model = _dataModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -16,7 +16,7 @@ public abstract partial class DetailsElementViewModel(IDetailsElement _detailsEl
public override void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -18,7 +18,7 @@ public partial class DetailsLinkViewModel(
public Uri? Link { get; private set; }
public bool IsLink => Link != null;
public bool IsLink => Link is not null;
public bool IsText => !IsLink;
@@ -26,14 +26,14 @@ public partial class DetailsLinkViewModel(
{
base.InitializeProperties();
var model = _dataModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
Text = model.Text ?? string.Empty;
Link = model.Link;
if (string.IsNullOrEmpty(Text) && Link != null)
if (string.IsNullOrEmpty(Text) && Link is not null)
{
Text = Link.ToString();
}

View File

@@ -22,7 +22,7 @@ public partial class DetailsTagsViewModel(
{
base.InitializeProperties();
var model = _dataModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -26,7 +26,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
public override void InitializeProperties()
{
var model = _detailsModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
@@ -41,7 +41,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
UpdateProperty(nameof(HeroImage));
var meta = model.Metadata;
if (meta != null)
if (meta is not null)
{
foreach (var element in meta)
{
@@ -53,7 +53,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
_ => null,
};
if (vm != null)
if (vm is not null)
{
vm.InitializeProperties();
Metadata.Add(vm);

View File

@@ -16,7 +16,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
// If the extension previously gave us a Data, then died, the data will
// throw if we actually try to read it, but the pointer itself won't be
// null, so this is relatively safe.
public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe != null;
public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe is not null;
// Locally cached properties from IIconData.
public string Icon { get; private set; } = string.Empty;
@@ -36,7 +36,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
public void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -26,7 +26,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
public bool HasIcon(bool light) => IconForTheme(light).HasIcon;
public bool IsSet => _model.Unsafe != null;
public bool IsSet => _model.Unsafe is not null;
IIconData? IIconInfo.Dark => Dark;
@@ -43,7 +43,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
public void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -27,7 +27,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
public DetailsViewModel? Details { get; private set; }
[MemberNotNullWhen(true, nameof(Details))]
public bool HasDetails => Details != null;
public bool HasDetails => Details is not null;
public override void InitializeProperties()
{
@@ -40,7 +40,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
base.InitializeProperties();
var li = Model.Unsafe;
if (li == null)
if (li is null)
{
return; // throw?
}
@@ -50,7 +50,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
TextToSuggest = li.TextToSuggest;
Section = li.Section ?? string.Empty;
var extensionDetails = li.Details;
if (extensionDetails != null)
if (extensionDetails is not null)
{
Details = new(extensionDetails, PageContext);
Details.InitializeProperties();
@@ -67,7 +67,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
base.FetchProperty(propertyName);
var model = this.Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -85,7 +85,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
@@ -136,7 +136,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
Details?.SafeCleanup();
var model = Model.Unsafe;
if (model != null)
if (model is not null)
{
// We don't need to revoke the PropChanged event handler here,
// because we are just overriding CommandItem's FetchProperty and

View File

@@ -63,6 +63,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
private Task? _initializeItemsTask;
private CancellationTokenSource? _cancellationTokenSource;
private CancellationTokenSource? _fetchItemsCancellationTokenSource;
private ListItemViewModel? _lastSelectedItem;
@@ -129,22 +130,38 @@ public partial class ListViewModel : PageViewModel, IDisposable
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchItems()
{
// Cancel any previous FetchItems operation
_fetchItemsCancellationTokenSource?.Cancel();
_fetchItemsCancellationTokenSource?.Dispose();
_fetchItemsCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _fetchItemsCancellationTokenSource.Token;
// TEMPORARY: just plop all the items into a single group
// see 9806fe5d8 for the last commit that had this with sections
_isFetching = true;
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
try
{
// Check for cancellation before starting expensive operations
cancellationToken.ThrowIfCancellationRequested();
var newItems = _model.Unsafe!.GetItems();
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
// Check for cancellation after getting items from extension
cancellationToken.ThrowIfCancellationRequested();
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
foreach (var item in newItems)
{
// Check for cancellation during item processing
cancellationToken.ThrowIfCancellationRequested();
ListItemViewModel viewModel = new(item, new(this));
// If an item fails to load, silently ignore it.
@@ -154,25 +171,57 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
}
// Check for cancellation before initializing first twenty items
cancellationToken.ThrowIfCancellationRequested();
var firstTwenty = newViewModels.Take(20);
foreach (var item in firstTwenty)
{
cancellationToken.ThrowIfCancellationRequested();
item?.SafeInitializeProperties();
}
// Cancel any ongoing search
_cancellationTokenSource?.Cancel();
// Check for cancellation before updating the list
cancellationToken.ThrowIfCancellationRequested();
List<ListItemViewModel> removedItems = [];
lock (_listLock)
{
// Now that we have new ViewModels for everything from the
// extension, smartly update our list of VMs
ListHelpers.InPlaceUpdateList(Items, newViewModels);
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
// you'll clean up list items that we've now transferred into
// .Items
}
// If we removed items, we need to clean them up, to remove our event handlers
foreach (var removedItem in removedItems)
{
removedItem.SafeCleanup();
}
// TODO: Iterate over everything in Items, and prune items from the
// cache if we don't need them anymore
}
catch (OperationCanceledException)
{
// Cancellation is expected, don't treat as error
// However, if we were cancelled, we didn't actually add these items to
// our Items list. Before we release them to the GC, make sure we clean
// them up
foreach (var vm in newViewModels)
{
vm.SafeCleanup();
}
return;
}
catch (Exception ex)
{
// TODO: Move this within the for loop, so we can catch issues with individual items
@@ -298,11 +347,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
[RelayCommand]
private void InvokeItem(ListItemViewModel? item)
{
if (item != null)
if (item is not null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
}
else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null)
else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe is not null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
EmptyContent.PrimaryCommand.Command.Model,
@@ -314,14 +363,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
[RelayCommand]
private void InvokeSecondaryCommand(ListItemViewModel? item)
{
if (item != null)
if (item is not null)
{
if (item.SecondaryCommand != null)
if (item.SecondaryCommand is not null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command.Model, item.Model));
}
}
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null)
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe is not null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
EmptyContent.SecondaryCommand.Command.Model,
@@ -332,12 +381,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
[RelayCommand]
private void UpdateSelectedItem(ListItemViewModel? item)
{
if (_lastSelectedItem != null)
if (_lastSelectedItem is not null)
{
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
if (item != null)
if (item is not null)
{
SetSelectedItem(item);
}
@@ -383,7 +432,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var item = _lastSelectedItem;
if (item == null)
if (item is null)
{
return;
}
@@ -438,7 +487,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
base.InitializeProperties();
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -465,7 +514,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
public void LoadMoreIfNeeded()
{
var model = this._model.Unsafe;
if (model == null)
if (model is null)
{
return;
}
@@ -509,7 +558,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
base.FetchProperty(propertyName);
var model = this._model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -540,7 +589,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
private void UpdateEmptyContent()
{
UpdateProperty(nameof(ShowEmptyContent));
if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null)
if (!ShowEmptyContent || EmptyContent.Model.Unsafe is null)
{
return;
}
@@ -560,6 +609,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
_fetchItemsCancellationTokenSource?.Cancel();
_fetchItemsCancellationTokenSource?.Dispose();
_fetchItemsCancellationTokenSource = null;
}
protected override void UnsafeCleanup()
@@ -570,6 +623,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(null), PageContext); // necessary?
_cancellationTokenSource?.Cancel();
_fetchItemsCancellationTokenSource?.Cancel();
lock (_listLock)
{
@@ -588,7 +642,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
var model = _model.Unsafe;
if (model != null)
if (model is not null)
{
model.ItemsChanged -= Model_ItemsChanged;
}

View File

@@ -22,7 +22,7 @@ public partial class LogMessageViewModel : ExtensionObjectViewModel
public override void InitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}

View File

@@ -5,6 +5,7 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -45,7 +46,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
[ObservableProperty]
public partial AppExtensionHost ExtensionHost { get; private set; }
public bool HasStatusMessage => MostRecentStatusMessage != null;
public bool HasStatusMessage => MostRecentStatusMessage is not null;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasStatusMessage))]
@@ -132,7 +133,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public override void InitializeProperties()
{
var page = _pageModel.Unsafe;
if (page == null)
if (page is null)
{
return; // throw?
}
@@ -177,7 +178,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
protected virtual void FetchProperty(string propertyName)
{
var model = this._pageModel.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -223,9 +224,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
extensionHint ??= ExtensionHost.GetExtensionDisplayName() ?? Title;
Task.Factory.StartNew(
() =>
{
ErrorMessage += $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
},
{
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint);
ErrorMessage += message;
},
CancellationToken.None,
TaskCreationOptions.None,
Scheduler);
@@ -240,7 +242,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged;
var model = _pageModel.Unsafe;
if (model != null)
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
}

View File

@@ -24,7 +24,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -50,7 +50,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel
protected virtual void FetchProperty(string propertyName)
{
var model = this.Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}

View File

@@ -120,7 +120,7 @@ public partial class ShellViewModel : ObservableObject,
////LoadedState = ViewModelLoadedState.Loading;
if (!viewModel.IsInitialized
&& viewModel.InitializeCommand != null)
&& viewModel.InitializeCommand is not null)
{
_ = Task.Run(async () =>
{
@@ -185,7 +185,7 @@ public partial class ShellViewModel : ObservableObject,
private void PerformCommand(PerformCommandMessage message)
{
var command = message.Command.Unsafe;
if (command == null)
if (command is null)
{
return;
}
@@ -205,7 +205,7 @@ public partial class ShellViewModel : ObservableObject,
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
if (pageViewModel == null)
if (pageViewModel is null)
{
Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
throw new NotSupportedException();
@@ -240,7 +240,7 @@ public partial class ShellViewModel : ObservableObject,
// TODO GH #525 This needs more better locking.
lock (_invokeLock)
{
if (_handleInvokeTask != null)
if (_handleInvokeTask is not null)
{
// do nothing - a command is already doing a thing
}
@@ -280,7 +280,7 @@ public partial class ShellViewModel : ObservableObject,
private void UnsafeHandleCommandResult(ICommandResult? result)
{
if (result == null)
if (result is null)
{
// No result, nothing to do.
return;

View File

@@ -17,7 +17,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
public ProgressViewModel? Progress { get; private set; }
public bool HasProgress => Progress != null;
public bool HasProgress => Progress is not null;
public StatusMessageViewModel(IStatusMessage message, WeakReference<IPageContext> context)
: base(context)
@@ -28,7 +28,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -36,7 +36,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
Message = model.Message;
State = model.State;
var modelProgress = model.Progress;
if (modelProgress != null)
if (modelProgress is not null)
{
Progress = new(modelProgress, this.PageContext);
Progress.InitializeProperties();
@@ -61,7 +61,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
protected virtual void FetchProperty(string propertyName)
{
var model = this.Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -76,7 +76,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
break;
case nameof(Progress):
var modelProgress = model.Progress;
if (modelProgress != null)
if (modelProgress is not null)
{
Progress = new(modelProgress, this.PageContext);
Progress.InitializeProperties();

View File

@@ -28,7 +28,7 @@ public partial class TagViewModel(ITag _tag, WeakReference<IPageContext> context
public override void InitializeProperties()
{
var model = _tagModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}

View File

@@ -35,7 +35,7 @@ public partial class AliasManager : ObservableObject
try
{
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
if (topLevelCommand != null)
if (topLevelCommand is not null)
{
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
@@ -88,7 +88,7 @@ public partial class AliasManager : ObservableObject
}
// If we already have _this exact alias_, do nothing
if (newAlias != null &&
if (newAlias is not null &&
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
{
if (existingAlias.CommandId == commandId)
@@ -113,7 +113,7 @@ public partial class AliasManager : ObservableObject
_aliases.Remove(alias.SearchPrefix);
}
if (newAlias != null)
if (newAlias is not null)
{
AddAlias(newAlias);
}

View File

@@ -55,7 +55,7 @@ public partial class AppStateModel : ObservableObject
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
return loaded ?? new();
}

View File

@@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class CommandProviderWrapper
{
public bool IsExtension => Extension != null;
public bool IsExtension => Extension is not null;
private readonly bool isValid;
@@ -188,14 +188,14 @@ public sealed class CommandProviderWrapper
return topLevelViewModel;
};
if (commands != null)
if (commands is not null)
{
TopLevelItems = commands
.Select(c => makeAndAdd(c, false))
.ToArray();
}
if (fallbacks != null)
if (fallbacks is not null)
{
FallbackItems = fallbacks
.Select(c => makeAndAdd(c, true))

View File

@@ -18,18 +18,18 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings,
public bool Initialized { get; private set; }
public bool HasSettings =>
_model.Unsafe != null && // We have a settings model AND
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
_model.Unsafe is not null && // We have a settings model AND
(!Initialized || SettingsPage is not null); // we weren't initialized, OR we were, and we do have a settings page
private void UnsafeInitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
if (model is null)
{
return;
}
if (model.SettingsPage != null)
if (model.SettingsPage is not null)
{
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost);
SettingsPage.InitializeProperties();

View File

@@ -30,7 +30,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
public override ICommandResult SubmitForm(string inputs, string data)
{
var dataInput = JsonNode.Parse(data)?.AsObject();
if (dataInput == null)
if (dataInput is null)
{
return CommandResult.KeepOpen();
}

View File

@@ -23,7 +23,7 @@ public partial class LogMessagesPage : ListPage
private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null)
{
foreach (var item in e.NewItems)
{

View File

@@ -203,7 +203,7 @@ public partial class MainListPage : DynamicListPage,
// If we don't have any previous filter results to work with, start
// with a list of all our commands & apps.
if (_filteredItems == null)
if (_filteredItems is null)
{
_filteredItems = commands;
_filteredItemsIncludesApps = _includeApps;

View File

@@ -98,7 +98,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
public override CommandResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload)?.AsObject();
if (formInput == null)
if (formInput is null)
{
return CommandResult.KeepOpen();
}

View File

@@ -14,7 +14,7 @@ public partial class NewExtensionPage : ContentPage
public override IContent[] GetContent()
{
return _resultForm != null ? [_resultForm] : [_inputForm];
return _resultForm is not null ? [_resultForm] : [_inputForm];
}
public NewExtensionPage()
@@ -28,13 +28,13 @@ public partial class NewExtensionPage : ContentPage
private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args)
{
if (_resultForm != null)
if (_resultForm is not null)
{
_resultForm.FormSubmitted -= FormSubmitted;
}
_resultForm = args;
if (_resultForm != null)
if (_resultForm is not null)
{
_resultForm.FormSubmitted += FormSubmitted;
}

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.Diagnostics.CodeAnalysis;
using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
@@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
UpdateProperty(nameof(Card));
}
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveSubmitAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveExecuteAction))]
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
{
// BODGY circa GH #40979
// Usually, you're supposed to try to cast the action to a specific
// type, and use those objects to get the data you need.
// However, there's something weird with AdaptiveCards and the way it
// works when we consume it when built in Release, with AOT (and
// trimming) enabled. Any sort of `action.As<IAdaptiveSubmitAction>()`
// or similar will throw a System.InvalidCastException.
//
// Instead we have this horror show.
//
// The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
// we can use to determine what kind of action it is. Then we can parse
// the JSON manually based on the type.
var actionJson = action.ToJson();
if (actionJson.TryGetValue("type", out var actionTypeValue))
if (action is AdaptiveOpenUrlAction openUrlAction)
{
var actionTypeString = actionTypeValue.GetString();
Logger.LogTrace($"atString={actionTypeString}");
var actionType = actionTypeString switch
{
"Action.Submit" => ActionType.Submit,
"Action.Execute" => ActionType.Execute,
"Action.OpenUrl" => ActionType.OpenUrl,
_ => ActionType.Unsupported,
};
Logger.LogDebug($"{actionTypeString}->{actionType}");
switch (actionType)
{
case ActionType.OpenUrl:
{
HandleOpenUrlAction(action, actionJson);
}
break;
case ActionType.Submit:
case ActionType.Execute:
{
HandleSubmitAction(action, actionJson, inputs);
}
break;
default:
Logger.LogError($"{actionType} was an unexpected action `type`");
break;
}
}
else
{
Logger.LogError($"actionJson.TryGetValue(type) failed");
}
}
private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
{
if (actionJson.TryGetValue("url", out var actionUrlValue))
{
var actionUrl = actionUrlValue.GetString() ?? string.Empty;
if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
{
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(uri));
}
else
{
Logger.LogError($"Failed to produce URI for {actionUrlValue}");
}
}
}
private void HandleSubmitAction(
IAdaptiveActionElement action,
JsonObject actionJson,
JsonObject inputs)
{
var dataString = string.Empty;
if (actionJson.TryGetValue("data", out var actionDataValue))
{
dataString = actionDataValue.Stringify() ?? string.Empty;
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
return;
}
var inputString = inputs.Stringify();
_ = Task.Run(() =>
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
{
try
// Get the data and inputs
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
var inputString = inputs.Stringify();
_ = Task.Run(() =>
{
var model = _formModel.Unsafe!;
if (model != null)
try
{
var result = model.SubmitForm(inputString, dataString);
Logger.LogDebug($"SubmitForm() returned {result}");
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
var model = _formModel.Unsafe!;
if (model != null)
{
var result = model.SubmitForm(inputString, dataString);
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
}
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
catch (Exception ex)
{
ShowException(ex);
}
});
}
}
private static readonly string ErrorCardJson = """

View File

@@ -20,7 +20,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return;
}
@@ -47,7 +47,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
protected void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -66,7 +66,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
{
base.UnsafeCleanup();
var model = Model.Unsafe;
if (model != null)
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
}

View File

@@ -30,13 +30,13 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return;
}
var root = model.RootContent;
if (root != null)
if (root is not null)
{
RootContent = ViewModelFromContent(root, PageContext);
RootContent?.InitializeProperties();
@@ -82,7 +82,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
protected void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model == null)
if (model is null)
{
return; // throw?
}
@@ -91,7 +91,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
{
case nameof(RootContent):
var root = model.RootContent;
if (root != null)
if (root is not null)
{
RootContent = ViewModelFromContent(root, PageContext);
}
@@ -119,7 +119,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
foreach (var item in newItems)
{
var viewModel = ViewModelFromContent(item, PageContext);
if (viewModel != null)
if (viewModel is not null)
{
viewModel.InitializeProperties();
newContent.Add(viewModel);
@@ -153,7 +153,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
Children.Clear();
var model = Model.Unsafe;
if (model != null)
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
model.ItemsChanged -= Model_ItemsChanged;

View File

@@ -29,7 +29,7 @@ public partial class HotkeyManager : ObservableObject
}
}
_commandHotkeys.RemoveAll(item => item.Hotkey == null);
_commandHotkeys.RemoveAll(item => item.Hotkey is null);
foreach (var item in _commandHotkeys)
{

View File

@@ -90,7 +90,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
}).Result;
var isExtension = isCmdPalExtensionResult.IsExtension;
var extension = isCmdPalExtensionResult.Extension;
if (isExtension && extension != null)
if (isExtension && extension is not null)
{
CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}");
@@ -152,7 +152,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
{
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
return new(cmdPalProvider != null && classId.Count != 0, extension);
return new(cmdPalProvider is not null && classId.Count != 0, extension);
}
}
@@ -237,7 +237,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
{
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
if (cmdPalProvider == null || classIds.Count == 0)
if (cmdPalProvider is null || classIds.Count == 0)
{
return [];
}
@@ -352,12 +352,12 @@ public partial class ExtensionService : IExtensionService, IDisposable
{
var propSetList = new List<string>();
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
if (singlePropertySet != null)
if (singlePropertySet is not null)
{
var classId = GetProperty(singlePropertySet, ClassIdProperty);
// If the instance has a classId as a single string, then it's only supporting a single instance.
if (classId != null)
if (classId is not null)
{
propSetList.Add(classId);
}
@@ -365,7 +365,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
else
{
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
if (propertySetArray != null)
if (propertySetArray is not null)
{
foreach (var prop in propertySetArray)
{
@@ -375,7 +375,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
}
var classId = GetProperty(propertySet, ClassIdProperty);
if (classId != null)
if (classId is not null)
{
propSetList.Add(classId);
}

View File

@@ -35,7 +35,7 @@ public class ProviderSettings
public void Connect(CommandProviderWrapper wrapper)
{
ProviderId = wrapper.ProviderId;
IsBuiltin = wrapper.Extension == null;
IsBuiltin = wrapper.Extension is null;
ProviderDisplayName = wrapper.DisplayName;

View File

@@ -33,7 +33,7 @@ public partial class ProviderSettingsViewModel(
Resources.builtin_disabled_extension;
[MemberNotNullWhen(true, nameof(Extension))]
public bool IsFromExtension => _provider.Extension != null;
public bool IsFromExtension => _provider.Extension is not null;
public IExtensionWrapper? Extension => _provider.Extension;
@@ -76,7 +76,7 @@ public partial class ProviderSettingsViewModel(
{
get
{
if (_provider.Settings == null)
if (_provider.Settings is null)
{
return false;
}
@@ -100,7 +100,7 @@ public partial class ProviderSettingsViewModel(
{
get
{
if (_provider.Settings == null)
if (_provider.Settings is null)
{
return null;
}
@@ -126,7 +126,7 @@ public partial class ProviderSettingsViewModel(
{
get
{
if (field == null)
if (field is null)
{
field = BuildTopLevelViewModels();
}
@@ -149,7 +149,7 @@ public partial class ProviderSettingsViewModel(
{
get
{
if (field == null)
if (field is null)
{
field = BuildFallbackViewModels();
}
@@ -173,7 +173,7 @@ public partial class ProviderSettingsViewModel(
private void InitializeSettingsPage()
{
if (_provider.Settings == null)
if (_provider.Settings is null)
{
return;
}

View File

@@ -30,7 +30,7 @@ public partial class RecentCommandsManager : ObservableObject
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
// match after one use.
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
if (entry.Item != null)
if (entry.Item is not null)
{
var index = entry.Index;
@@ -61,7 +61,7 @@ public partial class RecentCommandsManager : ObservableObject
var entry = History
.Where(item => item.CommandId == commandId)
.FirstOrDefault();
if (entry == null)
if (entry is null)
{
var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 };
History.Insert(0, newitem);

View File

@@ -95,7 +95,7 @@ public partial class SettingsModel : ObservableObject
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
return loaded ?? new();
}

View File

@@ -170,20 +170,29 @@ public partial class TopLevelCommandManager : ObservableObject,
// TODO: just added a lock around all of this anyway, but keeping the clone
// while looking on some other ways to improve this; can be removed later.
List<TopLevelViewModel> clone = [.. TopLevelCommands];
var startIndex = -1;
var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId);
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
clone.InsertRange(startIndex, newItems);
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
}
return;
static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId)
{
// Tricky: all Commands from a single provider get added to the
// top-level list all together, in a row. So if we find just the first
// one, we can slice it out and insert the new ones there.
for (var i = 0; i < clone.Count; i++)
for (var i = 0; i < topLevelItems.Count; i++)
{
var wrapper = clone[i];
var wrapper = topLevelItems[i];
try
{
if (sender.ProviderId == wrapper.CommandProviderId)
if (providerId == wrapper.CommandProviderId)
{
startIndex = i;
break;
return i;
}
}
catch
@@ -191,9 +200,8 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
clone.InsertRange(startIndex, newItems);
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
// If we didn't find any, then we just append the new commands to the end of the list.
return topLevelItems.Count;
}
}
@@ -241,7 +249,7 @@ public partial class TopLevelCommandManager : ObservableObject,
_extensionCommandProviders.Clear();
}
if (extensions != null)
if (extensions is not null)
{
await StartExtensionsAndGetCommands(extensions);
}
@@ -275,7 +283,7 @@ public partial class TopLevelCommandManager : ObservableObject,
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
// Wait for all extensions to start
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper != null).Select(w => w!).ToList();
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper is not null).Select(w => w!).ToList();
lock (_commandProvidersLock)
{
@@ -285,7 +293,7 @@ public partial class TopLevelCommandManager : ObservableObject,
// Load the commands from the providers in parallel
var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync);
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results != null).Select(r => r!).ToList();
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList();
lock (TopLevelCommands)
{
@@ -402,8 +410,8 @@ public partial class TopLevelCommandManager : ObservableObject,
void IPageContext.ShowException(Exception ex, string? extensionHint)
{
var errorMessage = $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
CommandPaletteHost.Instance.Log(errorMessage);
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");
CommandPaletteHost.Instance.Log(message);
}
internal bool IsProviderActive(string id)

View File

@@ -240,7 +240,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private void FetchAliasFromAliasManager()
{
var am = _serviceProvider.GetService<AliasManager>();
if (am != null)
if (am is not null)
{
var commandAlias = am.AliasFromId(Id);
if (commandAlias is not null)
@@ -254,7 +254,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private void UpdateHotkey()
{
var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
if (hotkey != null)
if (hotkey is not null)
{
_hotkey = hotkey.Hotkey;
}
@@ -264,12 +264,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
List<Tag> tags = [];
if (Hotkey != null)
if (Hotkey is not null)
{
tags.Add(new Tag() { Text = Hotkey.ToString() });
}
if (Alias != null)
if (Alias is not null)
{
tags.Add(new Tag() { Text = Alias.SearchPrefix });
}

View File

@@ -47,7 +47,8 @@
<Flyout
x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened">
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False">
<cpcontrols:ContextMenu x:Name="ContextControl" />
</Flyout>

View File

@@ -10,7 +10,6 @@ using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Windows.System;
namespace Microsoft.CmdPal.UI.Controls;
@@ -50,7 +49,7 @@ public sealed partial class CommandBar : UserControl,
return;
}
if (message.Element == null)
if (message.Element is null)
{
_ = DispatcherQueue.TryEnqueue(
() =>

View File

@@ -44,7 +44,7 @@ public sealed partial class ContentFormControl : UserControl
// 5% BODGY: if we set this multiple times over the lifetime of the app,
// then the second call will explode, because "CardOverrideStyles is already the child of another element".
// SO only set this once.
if (_renderer.OverrideStyles == null)
if (_renderer.OverrideStyles is null)
{
_renderer.OverrideStyles = CardOverrideStyles;
}
@@ -55,19 +55,19 @@ public sealed partial class ContentFormControl : UserControl
private void AttachViewModel(ContentFormViewModel? vm)
{
if (_viewModel != null)
if (_viewModel is not null)
{
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
}
_viewModel = vm;
if (_viewModel != null)
if (_viewModel is not null)
{
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
var c = _viewModel.Card;
if (c != null)
if (c is not null)
{
DisplayCard(c);
}
@@ -76,7 +76,7 @@ public sealed partial class ContentFormControl : UserControl
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (ViewModel == null)
if (ViewModel is null)
{
return;
}
@@ -84,7 +84,7 @@ public sealed partial class ContentFormControl : UserControl
if (e.PropertyName == nameof(ViewModel.Card))
{
var c = ViewModel.Card;
if (c != null)
if (c is not null)
{
DisplayCard(c);
}
@@ -95,7 +95,7 @@ public sealed partial class ContentFormControl : UserControl
{
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
ContentGrid.Children.Clear();
if (_renderedCard.FrameworkElement != null)
if (_renderedCard.FrameworkElement is not null)
{
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
@@ -148,7 +148,7 @@ public sealed partial class ContentFormControl : UserControl
// Recursively check children
var result = FindFirstFocusableElement(child);
if (result != null)
if (result is not null)
{
return result;
}

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.Diagnostics;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -35,6 +36,17 @@ public partial class ContentIcon : FontIcon
{
if (this.FindDescendants().OfType<Grid>().FirstOrDefault() is Grid grid && Content is not null)
{
if (grid.Children.Contains(Content))
{
return;
}
if (Content is FrameworkElement element && element.Parent is not null)
{
Debug.Assert(false, $"IconBoxElement Content is already parented to {element.Parent.GetType().Name}");
return;
}
grid.Children.Add(Content);
}
}

View File

@@ -112,7 +112,7 @@
<Rectangle
Height="1"
Margin="-16,-12,-12,-12"
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>

View File

@@ -31,7 +31,7 @@ public sealed partial class ContextMenu : UserControl,
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
if (ViewModel != null)
if (ViewModel is not null)
{
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}

View File

@@ -91,7 +91,7 @@ public partial class IconBox : ContentControl
{
if (d is IconBox @this)
{
if (e.NewValue == null)
if (e.NewValue is null)
{
@this.Source = null;
}
@@ -104,7 +104,7 @@ public partial class IconBox : ContentControl
var requestedTheme = @this.ActualTheme;
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
if (@this.SourceRequested != null)
if (@this.SourceRequested is not null)
{
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
@@ -142,7 +142,7 @@ public partial class IconBox : ContentControl
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
}
if (iconData != null &&
if (iconData is not null &&
@this.Source is FontIconSource)
{
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)

View File

@@ -2,7 +2,6 @@
// 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;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
@@ -80,12 +79,12 @@ public sealed partial class KeyVisual : Control
private void Update()
{
if (_keyVisual == null)
if (_keyVisual is null)
{
return;
}
if (_keyVisual.Content != null)
if (_keyVisual.Content is not null)
{
if (_keyVisual.Content.GetType() == typeof(string))
{

View File

@@ -51,13 +51,13 @@ public sealed partial class SearchBar : UserControl,
//// TODO: If the Debounce timer hasn't fired, we may want to store the current Filter in the OldValue/prior VM, but we don't want that to go actually do work...
var @this = (SearchBar)d;
if (@this != null
if (@this is not null
&& e.OldValue is PageViewModel old)
{
old.PropertyChanged -= @this.Page_PropertyChanged;
}
if (@this != null
if (@this is not null
&& e.NewValue is PageViewModel page)
{
// TODO: In some cases we probably want commands to clear a filter
@@ -85,7 +85,7 @@ public sealed partial class SearchBar : UserControl,
{
this.FilterBox.Text = string.Empty;
if (CurrentPageViewModel != null)
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.Filter = string.Empty;
}
@@ -143,7 +143,7 @@ public sealed partial class SearchBar : UserControl,
FilterBox.Text = string.Empty;
// hack TODO GH #245
if (CurrentPageViewModel != null)
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}
@@ -154,7 +154,7 @@ public sealed partial class SearchBar : UserControl,
else if (e.Key == VirtualKey.Back)
{
// hack TODO GH #245
if (CurrentPageViewModel != null)
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}
@@ -318,7 +318,7 @@ public sealed partial class SearchBar : UserControl,
}
// Actually plumb Filtering to the view model
if (CurrentPageViewModel != null)
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}

View File

@@ -39,13 +39,13 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs? e)
{
var me = d as ShortcutControl;
if (me == null)
if (me is null)
{
return;
}
var description = me.c?.FindDescendant<TextBlock>();
if (description == null)
if (description is null)
{
return;
}
@@ -431,7 +431,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (lastValidSettings != null && ComboIsValid(lastValidSettings))
if (lastValidSettings is not null && ComboIsValid(lastValidSettings))
{
HotkeySettings = lastValidSettings with { };
}
@@ -458,7 +458,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
private static bool ComboIsValid(HotkeySettings? settings)
{
return settings != null && (settings.IsValid() || settings.IsEmpty());
return settings is not null && (settings.IsValid() || settings.IsEmpty());
}
public void Receive(WindowActivatedEventArgs message) => DoWindowActivated(message);
@@ -466,12 +466,12 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
private void DoWindowActivated(WindowActivatedEventArgs args)
{
args.Handled = true;
if (args.WindowActivationState != WindowActivationState.Deactivated && (hook == null || hook.GetDisposedState() == true))
if (args.WindowActivationState != WindowActivationState.Deactivated && (hook is null || hook.GetDisposedState() == true))
{
// If the PT settings window gets focussed/activated again, we enable the keyboard hook to catch the keyboard input.
hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents);
}
else if (args.WindowActivationState == WindowActivationState.Deactivated && hook != null && hook.GetDisposedState() == false)
else if (args.WindowActivationState == WindowActivationState.Deactivated && hook is not null && hook.GetDisposedState() == false)
{
// If the PT settings window lost focus/activation, we disable the keyboard hook to allow keyboard input on other windows.
hook.Dispose();
@@ -490,7 +490,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
{
if (disposing)
{
if (hook != null)
if (hook is not null)
{
hook.Dispose();
}

View File

@@ -84,7 +84,7 @@ public partial class Tag : Control
return;
}
if (tag.ForegroundColor != null &&
if (tag.ForegroundColor is not null &&
OptionalColorBrushCacheProvider.Convert(tag.ForegroundColor.Value) is SolidColorBrush brush)
{
tag.Foreground = brush;
@@ -114,7 +114,7 @@ public partial class Tag : Control
return;
}
if (tag.BackgroundColor != null &&
if (tag.BackgroundColor is not null &&
OptionalColorBrushCacheProvider.Convert(tag.BackgroundColor.Value) is SolidColorBrush brush)
{
tag.Background = brush;

View File

@@ -34,8 +34,14 @@ public sealed partial class ContentPage : Page,
public ContentPage()
{
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
this.Unloaded += OnUnloaded;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
// Unhook from everything to ensure nothing can reach us
// between this point and our complete and utter destruction.
WeakReferenceMessenger.Default.UnregisterAll(this);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -45,6 +51,16 @@ public sealed partial class ContentPage : Page,
ViewModel = vm;
}
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSelectedListItemMessage>(this))
{
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
}
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSecondaryCommandMessage>(this))
{
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
}
base.OnNavigatedTo(e);
}

View File

@@ -15,6 +15,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.System;
namespace Microsoft.CmdPal.UI;
@@ -24,6 +25,8 @@ public sealed partial class ListPage : Page,
IRecipient<ActivateSelectedListItemMessage>,
IRecipient<ActivateSecondaryCommandMessage>
{
private InputSource _lastInputSource;
private ListViewModel? ViewModel
{
get => (ListViewModel?)GetValue(ViewModelProperty);
@@ -39,6 +42,8 @@ public sealed partial class ListPage : Page,
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Disabled;
this.ItemsList.Loaded += ItemsList_Loaded;
this.ItemsList.PreviewKeyDown += ItemsList_PreviewKeyDown;
this.ItemsList.PointerPressed += ItemsList_PointerPressed;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -74,7 +79,7 @@ public sealed partial class ListPage : Page,
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
if (ViewModel != null)
if (ViewModel is not null)
{
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
@@ -98,6 +103,12 @@ public sealed partial class ListPage : Page,
{
if (e.ClickedItem is ListItemViewModel item)
{
if (_lastInputSource == InputSource.Keyboard)
{
ViewModel?.InvokeItemCommand.Execute(item);
return;
}
var settings = App.Current.Services.GetService<SettingsModel>()!;
if (settings.SingleClickActivates)
{
@@ -142,13 +153,13 @@ public sealed partial class ListPage : Page,
// here, then in Page_ItemsUpdated trying to select that cached item if
// it's in the list (otherwise, clear the cache), but that seems
// aggressively BODGY for something that mostly just works today.
if (ItemsList.SelectedItem != null)
if (ItemsList.SelectedItem is not null)
{
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
// Automation notification for screen readers
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemsList);
if (listViewPeer != null && li != null)
if (listViewPeer is not null && li is not null)
{
var notificationText = li.Title;
listViewPeer.RaiseNotificationEvent(
@@ -165,7 +176,7 @@ public sealed partial class ListPage : Page,
// Find the ScrollViewer in the ListView
var listViewScrollViewer = FindScrollViewer(this.ItemsList);
if (listViewScrollViewer != null)
if (listViewScrollViewer is not null)
{
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
}
@@ -174,7 +185,7 @@ public sealed partial class ListPage : Page,
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
{
var scrollView = sender as ScrollViewer;
if (scrollView == null)
if (scrollView is null)
{
return;
}
@@ -256,7 +267,7 @@ public sealed partial class ListPage : Page,
page.PropertyChanged += @this.ViewModel_PropertyChanged;
page.ItemsUpdated += @this.Page_ItemsUpdated;
}
else if (e.NewValue == null)
else if (e.NewValue is null)
{
Logger.LogDebug("cleared view model");
}
@@ -274,7 +285,7 @@ public sealed partial class ListPage : Page,
// ItemsList_SelectionChanged again to give us another chance to change
// the selection from null -> something. Better to just update the
// selection once, at the end of all the updating.
if (ItemsList.SelectedItem == null)
if (ItemsList.SelectedItem is null)
{
ItemsList.SelectedIndex = 0;
}
@@ -307,7 +318,7 @@ public sealed partial class ListPage : Page,
{
var child = VisualTreeHelper.GetChild(parent, i);
var result = FindScrollViewer(child);
if (result != null)
if (result is not null)
{
return result;
}
@@ -329,7 +340,7 @@ public sealed partial class ListPage : Page,
_ => (null, null),
};
if (item == null || element == null)
if (item is null || element is null)
{
return;
}
@@ -363,4 +374,21 @@ public sealed partial class ListPage : Page,
{
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
}
private void ItemsList_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
private void ItemsList_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
{
_lastInputSource = InputSource.Keyboard;
}
}
private enum InputSource
{
None,
Keyboard,
Pointer,
}
}

View File

@@ -2,13 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.UI.Xaml.Documents;
using Microsoft.Win32;
namespace Microsoft.CmdPal.UI.Helpers;
@@ -63,7 +56,7 @@ internal static class GpoValueChecker
{
using (RegistryKey? key = rootKey.OpenSubKey(subKeyPath, false))
{
if (key == null)
if (key is null)
{
return null;
}

View File

@@ -4,7 +4,6 @@
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Helpers;
namespace Microsoft.CmdPal.UI.Helpers;
@@ -19,7 +18,7 @@ public static partial class IconCacheProvider
public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
#pragma warning restore IDE0060 // Remove unused parameter
{
if (args.Key == null)
if (args.Key is null)
{
return;
}

View File

@@ -28,7 +28,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
return source;
}
else if (icon.Data != null)
else if (icon.Data is not null)
{
try
{
@@ -49,7 +49,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
private async Task<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
{
if (iconStreamRef == null)
if (iconStreamRef is null)
{
return null;
}

View File

@@ -0,0 +1,157 @@
// 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 ManagedCommon;
using Windows.System;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// A class that listens for local keyboard events using a Windows hook.
/// </summary>
internal sealed class LocalKeyboardListener : IDisposable
{
/// <summary>
/// Event that is raised when a key is pressed down.
/// </summary>
public event EventHandler<LocalKeyboardListenerKeyPressedEventArgs>? KeyPressed;
private bool _disposed;
private UnhookWindowsHookExSafeHandle? _handle;
private HOOKPROC? _hookProc; // Keep reference to prevent GC collection
/// <summary>
/// Registers a global keyboard hook to listen for key down events.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Throws if the hook could not be registered, which may happen if the system is unable to set the hook.
/// </exception>
public void RegisterKeyboardHook()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_handle is not null && !_handle.IsInvalid)
{
// Hook is already set
return;
}
_hookProc = KeyEventHook;
if (!SetWindowKeyHook(_hookProc))
{
throw new InvalidOperationException("Failed to register keyboard hook.");
}
}
/// <summary>
/// Attempts to register a global keyboard hook to listen for key down events.
/// </summary>
/// <returns>
/// <see langword="true"/> if the keyboard hook was successfully registered; otherwise, <see langword="false"/>.
/// </returns>
public bool Start()
{
if (_disposed)
{
return false;
}
try
{
RegisterKeyboardHook();
return true;
}
catch (Exception ex)
{
Logger.LogError("Failed to register hook", ex);
return false;
}
}
private void UnregisterKeyboardHook()
{
if (_handle is not null && !_handle.IsInvalid)
{
// The SafeHandle should automatically call UnhookWindowsHookEx when disposed
_handle.Dispose();
_handle = null;
}
_hookProc = null;
}
private bool SetWindowKeyHook(HOOKPROC hookProc)
{
if (_handle is not null && !_handle.IsInvalid)
{
// Hook is already set
return false;
}
_handle = PInvoke.SetWindowsHookEx(
WINDOWS_HOOK_ID.WH_KEYBOARD,
hookProc,
PInvoke.GetModuleHandle(null),
PInvoke.GetCurrentThreadId());
// Check if the hook was successfully set
return _handle is not null && !_handle.IsInvalid;
}
private static bool IsKeyDownHook(LPARAM lParam)
{
// The 30th bit tells what the previous key state is with 0 being the "UP" state
// For more info see https://learn.microsoft.com/windows/win32/winmsg/keyboardproc#lparam-in
return ((lParam.Value >> 30) & 1) == 0;
}
private LRESULT KeyEventHook(int nCode, WPARAM wParam, LPARAM lParam)
{
try
{
if (nCode >= 0 && IsKeyDownHook(lParam))
{
InvokeKeyDown((VirtualKey)wParam.Value);
}
}
catch (Exception ex)
{
Logger.LogError("Failed when invoking key down keyboard hook event", ex);
}
// Call next hook in chain - pass null as first parameter for current hook
return PInvoke.CallNextHookEx(null, nCode, wParam, lParam);
}
private void InvokeKeyDown(VirtualKey virtualKey)
{
if (!_disposed)
{
KeyPressed?.Invoke(this, new LocalKeyboardListenerKeyPressedEventArgs(virtualKey));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
UnregisterKeyboardHook();
}
_disposed = true;
}
}
}

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.
using Windows.System;
namespace Microsoft.CmdPal.UI.Helpers;
public class LocalKeyboardListenerKeyPressedEventArgs(VirtualKey key) : EventArgs
{
public VirtualKey Key { get; } = key;
}

View File

@@ -50,7 +50,7 @@ internal sealed partial class TrayIconService
{
if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon)
{
if (_window == null)
if (_window is null)
{
_window = new Window();
_hwnd = new HWND(WindowNative.GetWindowHandle(_window));
@@ -64,7 +64,7 @@ internal sealed partial class TrayIconService
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
}
if (_trayIconData == null)
if (_trayIconData is null)
{
// We need to stash this handle, so it doesn't clean itself up. If
// explorer restarts, we'll come back through here, and we don't
@@ -88,7 +88,7 @@ internal sealed partial class TrayIconService
// Add the notification icon
PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
if (_popupMenu == null)
if (_popupMenu is null)
{
_popupMenu = PInvoke.CreatePopupMenu_SafeHandle();
PInvoke.InsertMenu(_popupMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, RS_.GetString("TrayMenu_Settings"));
@@ -103,7 +103,7 @@ internal sealed partial class TrayIconService
public void Destroy()
{
if (_trayIconData != null)
if (_trayIconData is not null)
{
var d = (NOTIFYICONDATAW)_trayIconData;
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
@@ -112,19 +112,19 @@ internal sealed partial class TrayIconService
}
}
if (_popupMenu != null)
if (_popupMenu is not null)
{
_popupMenu.Close();
_popupMenu = null;
}
if (_largeIcon != null)
if (_largeIcon is not null)
{
_largeIcon.Close();
_largeIcon = null;
}
if (_window != null)
if (_window is not null)
{
_window.Close();
_window = null;
@@ -167,7 +167,7 @@ internal sealed partial class TrayIconService
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
case PInvoke.WM_WINDOWPOSCHANGING:
{
if (_trayIconData == null)
if (_trayIconData is null)
{
SetupTrayIcon();
}
@@ -189,7 +189,7 @@ internal sealed partial class TrayIconService
{
case PInvoke.WM_RBUTTONUP:
{
if (_popupMenu != null)
if (_popupMenu is not null)
{
PInvoke.GetCursorPos(out var cursorPos);
PInvoke.SetForegroundWindow(_hwnd);

View File

@@ -46,7 +46,7 @@ public static class TypedEventHandlerExtensions
#pragma warning restore CA1715 // Identifiers should have correct prefix
where R : DeferredEventArgs
{
if (eventHandler == null)
if (eventHandler is null)
{
return Task.CompletedTask;
}

View File

@@ -13,7 +13,7 @@ internal sealed partial class WindowHelper
UserNotificationState state;
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) == null)
if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) is null)
{
if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
state == UserNotificationState.QUNS_BUSY ||

View File

@@ -27,6 +27,7 @@ using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
@@ -44,7 +45,8 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>
IRecipient<QuitMessage>,
IDisposable
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
@@ -54,6 +56,7 @@ public sealed partial class MainWindow : WindowEx,
private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = [];
private readonly KeyboardListener _keyboardListener;
private readonly LocalKeyboardListener _localKeyboardListener;
private bool _ignoreHotKeyWhenFullScreen = true;
private DesktopAcrylicController? _acrylicController;
@@ -116,6 +119,18 @@ public sealed partial class MainWindow : WindowEx,
{
Summon(string.Empty);
});
_localKeyboardListener = new LocalKeyboardListener();
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
_localKeyboardListener.Start();
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
{
WeakReferenceMessenger.Default.Send(new GoBackMessage());
}
}
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
@@ -376,6 +391,7 @@ public sealed partial class MainWindow : WindowEx,
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
// Workaround by turning it off before shutdown.
App.Current.DebugSettings.FailFastOnErrors = false;
_localKeyboardListener.Dispose();
DisposeAcrylic();
_keyboardListener.Stop();
@@ -384,7 +400,7 @@ public sealed partial class MainWindow : WindowEx,
private void DisposeAcrylic()
{
if (_acrylicController != null)
if (_acrylicController is not null)
{
_acrylicController.Dispose();
_acrylicController = null!;
@@ -459,7 +475,7 @@ public sealed partial class MainWindow : WindowEx,
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
}
if (_configurationSource != null)
if (_configurationSource is not null)
{
_configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
}
@@ -467,7 +483,7 @@ public sealed partial class MainWindow : WindowEx,
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
{
if (activatedEventArgs == null)
if (activatedEventArgs is null)
{
Summon(string.Empty);
return;
@@ -535,7 +551,7 @@ public sealed partial class MainWindow : WindowEx,
UnregisterHotkeys();
var globalHotkey = settings.Hotkey;
if (globalHotkey != null)
if (globalHotkey is not null)
{
if (settings.UseLowLevelGlobalHotkey)
{
@@ -565,7 +581,7 @@ public sealed partial class MainWindow : WindowEx,
{
var key = commandHotkey.Hotkey;
if (key != null)
if (key is not null)
{
if (settings.UseLowLevelGlobalHotkey)
{
@@ -682,4 +698,10 @@ public sealed partial class MainWindow : WindowEx,
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
public void Dispose()
{
_localKeyboardListener.Dispose();
DisposeAcrylic();
}
}

View File

@@ -47,4 +47,10 @@ DWM_CLOAKED_APP
CoWaitForMultipleObjects
INFINITE
CWMO_FLAGS
CWMO_FLAGS
GetCurrentThreadId
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx
GetModuleHandle

View File

@@ -24,7 +24,7 @@ public sealed partial class LoadingPage : Page
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is ShellViewModel shellVM
&& shellVM.LoadCommand != null)
&& shellVM.LoadCommand is not null)
{
// This will load the built-in commands, then navigate to the main page.
// Once the mainpage loads, we'll start loading extensions.

View File

@@ -171,7 +171,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// This gets called from the UI thread
private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args)
{
if (args == null)
if (args is null)
{
return;
}
@@ -236,7 +236,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public void OpenSettings()
{
if (_settingsWindow == null)
if (_settingsWindow is null)
{
_settingsWindow = new SettingsWindow();
}
@@ -324,7 +324,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// command from our list of toplevel commands.
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var topLevelCommand = tlcManager.LookupCommand(commandId);
if (topLevelCommand != null)
if (topLevelCommand is not null)
{
var command = topLevelCommand.CommandViewModel.Model.Unsafe;
var isPage = command is not IInvokableCommand;

View File

@@ -100,7 +100,7 @@ internal sealed class PowerToysRootPageService : IRootPageService
_activeExtension = extension;
var extensionWinRtObject = _activeExtension?.GetExtensionObject();
if (extensionWinRtObject != null)
if (extensionWinRtObject is not null)
{
try
{

View File

@@ -105,6 +105,8 @@ public sealed partial class SettingsWindow : WindowEx,
private void Window_Closed(object sender, WindowEventArgs args)
{
WeakReferenceMessenger.Default.Send<SettingsWindowClosedMessage>();
WeakReferenceMessenger.Default.UnregisterAll(this);
}
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class AllAppsCommandProviderTests : AppsTestBase
{
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new AllAppsCommandProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new AllAppsCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new AllAppsCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void LookupAppWithEmptyNameReturnsNotNull()
{
// Setup
var mockApp = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
MockCache.AddWin32Program(mockApp);
var page = new AllAppsPage(MockCache);
var provider = new AllAppsCommandProvider(page);
// Act
var result = provider.LookupApp(string.Empty);
// Assert
Assert.IsNotNull(result);
}
[TestMethod]
public async Task ProviderWithMockData_LookupApp_ReturnsCorrectApp()
{
// Arrange
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
MockCache.AddWin32Program(testApp);
var provider = new AllAppsCommandProvider(Page);
// Wait for initialization to complete
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("TestApp");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("TestApp", result.Title);
}
[TestMethod]
public async Task ProviderWithMockData_LookupApp_ReturnsNullForNonExistentApp()
{
// Arrange
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
MockCache.AddWin32Program(testApp);
var provider = new AllAppsCommandProvider(Page);
// Wait for initialization to complete
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("NonExistentApp");
// Assert
Assert.IsNull(result);
}
[TestMethod]
public void ProviderWithMockData_TopLevelCommands_IncludesListItem()
{
// Arrange
var provider = new AllAppsCommandProvider(Page);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length >= 1); // At least the list item should be present
}
}

View File

@@ -0,0 +1,97 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class AllAppsPageTests : AppsTestBase
{
[TestMethod]
public void AllAppsPage_Constructor_ThrowsOnNullAppCache()
{
// Act & Assert
Assert.ThrowsException<ArgumentNullException>(() => new AllAppsPage(null!));
}
[TestMethod]
public void AllAppsPage_WithMockCache_InitializesSuccessfully()
{
// Arrange
var mockCache = new MockAppCache();
// Act
var page = new AllAppsPage(mockCache);
// Assert
Assert.IsNotNull(page);
Assert.IsNotNull(page.Name);
Assert.IsNotNull(page.Icon);
}
[TestMethod]
public async Task AllAppsPage_GetItems_ReturnsEmptyWithEmptyCache()
{
// Act - Wait for initialization to complete
await WaitForPageInitializationAsync();
var items = Page.GetItems();
// Assert
Assert.IsNotNull(items);
Assert.AreEqual(0, items.Length);
}
[TestMethod]
public async Task AllAppsPage_GetItems_ReturnsAppsFromCacheAsync()
{
// Arrange
var mockCache = new MockAppCache();
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
mockCache.AddWin32Program(win32App);
mockCache.AddUWPApplication(uwpApp);
var page = new AllAppsPage(mockCache);
// Wait a bit for initialization to complete
await Task.Delay(100);
// Act
var items = page.GetItems();
// Assert
Assert.IsNotNull(items);
Assert.AreEqual(2, items.Length);
// we need to loop the items to ensure we got the correct ones
Assert.IsTrue(items.Any(i => i.Title == "Notepad"));
Assert.IsTrue(items.Any(i => i.Title == "Calculator"));
}
[TestMethod]
public async Task AllAppsPage_GetPinnedApps_ReturnsEmptyWhenNoAppsArePinned()
{
// Arrange
var mockCache = new MockAppCache();
var app = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
mockCache.AddWin32Program(app);
var page = new AllAppsPage(mockCache);
// Wait a bit for initialization to complete
await Task.Delay(100);
// Act
var pinnedApps = page.GetPinnedApps();
// Assert
Assert.IsNotNull(pinnedApps);
Assert.AreEqual(0, pinnedApps.Length);
}
}

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