Compare commits

...

32 Commits

Author SHA1 Message Date
Michael Clayton
3f6c47dddb Merge branch 'microsoft:main' into dev/mikeclayton/mwb-common-refactor 2025-08-12 15:48:55 +01:00
Yu Leng
c23dcb0c5a [CmdPal][UT] Refactor some cmdpal ext's ut and improve the test case (#40896)
<!-- 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. Remove some AI generated nonsense case
2. Add ISettingsInterface for those ext for testing purpose.
3. Add query test.

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

- [x] Closes: #40897
- [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>
2025-08-12 18:27:10 +08:00
Kai Tao
3682f186e3 Mouse highlighter spotlight mode, fix the gpu perf issue (#41079)
<!-- 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 big border solution for implementing the spotlight mode is gpu
consuming, switch to a resource friendly implementation.

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

| Before | After | 
|----------|----------|
| <img width="739" height="263" alt="image"
src="https://github.com/user-attachments/assets/b369801a-4cda-44e8-968b-d76586931c8c"
/>| <img width="1031" height="319" alt="image"
src="https://github.com/user-attachments/assets/9b21c96d-f5ce-4ff7-8662-0c4e6e075976"
/>|
2025-08-12 10:55:24 +08:00
Yu Leng
1eae1d9a12 [AOT] Remove rd.xml from CmdPal (#41031)
<!-- 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
Actually, we don't need to use rd.xml to preserve type. We can add
attribute to do the same things.

Tested in this build:
https://microsoft.visualstudio.com/Dart/_build/results?buildId=127875892&view=artifacts&pathAsName=false&type=publishedArtifacts

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

- [x] Closes: #40633
- [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 (from Dev Box) <yuleng@microsoft.com>
2025-08-12 07:44:23 +08:00
Mike Griese
bc134b344b CmdPal: Make sure to include apps in the ctor (#41081)
A regression from #40132 

We need to set the value of `_includeApps` in the `MainListPage` ctor.
Without it, if the user doesn't have any extensions installed, the value
is never updated.
2025-08-12 07:43:08 +08:00
Jiří Polášek
04b8234192 CmdPal: Fix styles applied to MoreCommandsButton (#41059)
## Summary of the Pull Request

- Apply the same padding to the button as used for primary and secondary
command buttons.
- Use consistent spacing between keycap blocks.
- Match keycap border style and inner text brush with other command
buttons.
- Add min width constraint to shortcut keycap element to make it at
least square.

<img width="961" height="355" alt="image"
src="https://github.com/user-attachments/assets/cff5ef7e-fe67-41ac-9796-063c0e69768a"
/>


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

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

## Detailed Description of the Pull Request / Additional comments

## Validation Steps Performed
👀
2025-08-11 18:12:05 -05:00
Shawn Yuan
d72e0ab20d Fixed toggle switch not working issue. (#41049)
<!-- 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
Fixed toggle switch not working issue.

## AI Summary
This pull request refactors how `DashboardListItem` objects are created
and added to collections in the `DashboardViewModel`. The main
improvement is to separate the instantiation of each `DashboardListItem`
from the assignment of its `EnabledChangedCallback` property, which is
now set after the object is added to the relevant collection. This
change improves clarity and may help prevent issues related to object
initialization order.

Refactoring of `DashboardListItem` creation and initialization:

* In the `AddDashboardListItem` method, the `DashboardListItem` object
is now created and added to `AllModules` before its
`EnabledChangedCallback` property is set, instead of setting this
property during object initialization.
* In the `GetShortcutModules` method, both `ShortcutModules` and
`ActionModules` collections now receive `DashboardListItem` objects that
are instantiated first, added to the collection, and then have their
`EnabledChangedCallback` property set. This replaces the previous
pattern of setting the callback during object creation.
[[1]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL123-R136)
[[2]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL144-R159)
* 
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41046
- [ ] **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
It is an regression from
https://github.com/microsoft/PowerToys/pull/40214

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

---------

Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-08-08 22:55:00 +08:00
Kai Tao
062234c295 Settings: Mouse utils setting crash (#41050)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fix a crash in settings page due to not found converter
<!-- Please review the items on the PR checklist before submitting-->

## AI Summary
This pull request makes a small update to the `MouseUtilsPage.xaml` file
to use the correct resource for converting boolean values to visibility
states in the UI.

- Updated the `Visibility` binding on an `InfoBar` to use the
`ReverseBoolToVisibilityConverter` instead of the incorrect
`BoolToReverseVisibilityConverter` resource.

## 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
Regression caused by https://github.com/microsoft/PowerToys/pull/40214

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
<img width="1983" height="1072" alt="image"
src="https://github.com/user-attachments/assets/13c806c3-e7af-4615-a649-6d58d8fe877b"
/>
2025-08-08 16:38:46 +08:00
Jeremy Sinclair
0d4f3d851e [Deps] Update .NET packages from 9.0.7 to 9.0.8 (#41039)
<!-- 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
Updates .NET 9 Runtime / Library packages to the latest 9.0.8 servicing
release.

<!-- 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-08 08:28:01 +08:00
Mike Griese
e93b044f39 CmdPal: Once again, I am asking you to fix form submits (#41010)
Closes #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.
2025-08-06 19:41:02 -05:00
leileizhang
fed6e523b6 Fix: used wrong preview resize event from another handler (#40995)
<!-- 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
Bug: Was using GcodePreviewResizeEvent, which will never work — switched
to use Bgcode's own event

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

## AI Summary
This pull request makes a minor update to the event handling in the
preview pane module. The change updates the event constant used for
resizing the preview from `GcodePreviewResizeEvent` to
`BgcodePreviewResizeEvent`, likely to improve naming consistency or to
support a new event type.
2025-08-06 14:12:37 +08:00
Jiří Polášek
0997c1a013 CmdPal: Coalesce top-level commands list changes into a single task (#40943)
## Summary of the Pull Request

Self-refresh of `MainListPage` introduced in #40132 causes unnecessary
spawning of tasks by `ReapplySearchInBackground` and pushing the code
down the scenic route instead of taking shortcut.

This drop-in fix introduces a single-worker coalescing refresh loop to
eliminate thread-pool churn and syncs state in early-return paths.

## PR Checklist

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

<!-- 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-05 16:26:50 -05:00
Jiří Polášek
fa55cdb67f CmdPal: properly dispose of the old backdrop controller (#40986)
## Summary of the Pull Request

Properly disposes the old DesktopAcrylicController when replacing it
with a new instance.

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

## Detailed Description of the Pull Request / Additional comments


## Validation Steps Performed
2025-08-05 16:26:22 -05:00
Jiří Polášek
a889f4d4bd CmdPal: Update a code comment using a wrong member name [nit] (#40987)
## Summary of the Pull Request

(see title)

<!-- 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-05 16:26:05 -05:00
Mike Griese
281c88a620 CmdPal: fix files not having an open command (#40990)
Yea, it's that dumb.

Regressed in #40768
2025-08-05 14:15:52 -05:00
Davide Giacometti
7bcddfeb09 [PowerRename] Fix named pipe detection (#40944)
<!-- 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

Fix a regression present on master where PowerRename is activated with
empty file list where invoked Explorer context menu.
Regression was caused by
https://github.com/microsoft/PowerToys/pull/40393

- [ ] 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

Verified that PowerRename shows file list when activated:
- From Windows 11 Explorer context menu
- From Legacy Explorer context menu
- From command line passing some file paths
2025-08-05 15:44:58 +08:00
Michael Jolley
7fb4ac2dcd Adding additional descriptions for all apps settings (#40911)
Closes #38351

Adding some descriptions for all apps extension settings.

<img width="725" height="470" alt="image"
src="https://github.com/user-attachments/assets/9fb06105-80a3-4c78-b10d-241164fead11"
/>
2025-08-04 18:34:20 -05:00
Niels Laute
c91bef1517 [UX] New dashboard & refactored KeyVisual (#40214)
### Updated `KeyVisual` and `Shortcut` control
- Refactoring `KeyVisual` to remove redundant properties and UI
elements, and using Styles for better customization.
- Shortcut control now shows a "Configure shortcut" label when there's
no shortcut configured.

### Other changes
- Consolidated converters that were used across pages in `App.xaml.cs`
with consistent naming.
- Renamed templated controls (from `.cs` to `.xaml.cs`) and moving those
to the `Controls` root folder vs. individual folders for a better
overview.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

Closes #39520
Closes #32944

---------

Co-authored-by: Jay <65828559+Jay-o-Way@users.noreply.github.com>
Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
2025-08-04 18:33:19 -05:00
leileizhang
fdd1f47d85 [UI tests] Fix UI test pipeline to properly handle buildSource parameter conditions (#40899)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fixes a logic issue in the UI test automation pipeline where selecting
`latestMainOfficialBuild` would still trigger a full PowerToys build
instead of only building UI tests.

### Why
The pipeline was using template variables in compilation-time
conditions, which doesn't work correctly in pipeline

### fix
Replace template variable references with direct parameter comparisons
in compilation-time conditions

<!-- 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-01 13:57:39 +08:00
leileizhang
9a998b2056 [UI Tests] Enhance UI Test Automation and Pipeline Support for CmdPal Module (#40871)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request introduces enhancements to UI test automation,
improvements to pipeline configuration, and project structure updates.
The goal is to improve flexibility, maintainability, and efficiency in
PowerToys’ CI/CD processes.

### UI Test Enhancements:
Delayed Text Input Support
- UI tests now support character-by-character text input with
configurable delays.
- This serves as a workaround for a known CmdPal bug where input is
swallowed too quickly. The delay mitigates the issue until it is fixed
in CmdPal.
 
Centralized Environment Management
- Introduced a new class to centralize environment variable access for
UI test configuration.

CmdPal Launch Handling in Pipelines

- Adjusted test logic to handle CmdPal module startup specifically in CI
pipelines



### Pipeline Configuration Updates:
Build Artifact Customization

- Included test-related folders in pipeline build outputs for better
traceability.

Support for Build ID Targeting

- Added support for specifying PowerToys build IDs in test pipelines,
with conditional logic for specific or latest build selection.
<img width="264" height="44" alt="image"
src="https://github.com/user-attachments/assets/0d68a51e-e41a-4868-a1c3-f4233c56b0ee"
/>


### Project Structure Updates:
Added Peek.UITests back to the solution which removed by
https://github.com/microsoft/PowerToys/pull/40754

<!-- 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-07-31 13:55:23 +08:00
Kai Tao
d26ef36e31 [Doc] Add doc for a script to build installer locally, and doc for testing winget install locally (#40805)
<!-- 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
1. Add instructions to build installer locally with a script
2. Add doc explaining how to install an installer by winget locally.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-07-31 09:48:24 +08:00
Jiří Polášek
ee6336c47d Change filter box placeholder for main list page only (#40799)
## Summary of the Pull Request

Changes the placeholder in the filter box only on the main list page to
"Search for apps, files and commands...":
<img width="786" height="473" alt="image"
src="https://github.com/user-attachments/assets/844d27ae-61cf-42c9-a7f6-ae78817e928c"
/>

The default value remains unchanged as "Type here to search..." for all
other pages (both built-in and internal), unless the author overrides
it:
<img width="786" height="473" alt="image"
src="https://github.com/user-attachments/assets/aeb3500b-9e36-4e35-8dd7-3bd226336823"
/>

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

- [x] Closes: #40763
- [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
- [x] **New binaries:** none
- [x] **Documentation updated:** 

## Detailed Description of the Pull Request / Additional comments

## Validation Steps Performed
2025-07-30 09:27:02 -07:00
Michael Clayton
740e0807d0 Merge branch 'microsoft:main' into dev/mikeclayton/mwb-common-refactor 2025-07-08 18:09:22 +01:00
Michael Clayton
c8c895e666 Merge branch 'microsoft:main' into dev/mikeclayton/mwb-common-refactor 2025-05-27 21:21:31 +01:00
mikeclayton
660bafb3d9 [MWB] - make InitAndClean static (#35155) 2025-03-24 00:00:03 +00:00
mikeclayton
600274dab8 [MWB] - fixing unit test (#35155) 2025-03-23 23:57:12 +00:00
mikeclayton
0fdabdfcb4 [MWB] - making InitAndCleanup members private (#35155) 2025-03-23 23:46:56 +00:00
mikeclayton
49ac04994b [MWB] - fixed references to InitAndCleanup.cs (#35155) 2025-03-23 23:43:52 +00:00
mikeclayton
2e763c41d5 [MWB] - moving "Common.InitAndCleanup.cs" -> Core/InitAndCleanup.cs (#35155) 2025-03-23 23:34:30 +00:00
mikeclayton
05c9edd8c3 [MWB] - making Clipboard.cs members private (#35155) 2025-03-23 23:33:14 +00:00
mikeclayton
f372a2ef0d [MWB] - fixed references to Clipboard.cs (#35155) 2025-03-23 23:29:10 +00:00
mikeclayton
887d9d9bb7 [MWB] - moving "Common.Clipboard.cs" -> Core/Clipboard.cs (#35155) 2025-03-23 23:12:33 +00:00
150 changed files with 4744 additions and 3778 deletions

View File

@@ -1440,6 +1440,7 @@ secpol
securestring
SEEMASKINVOKEIDLIST
SELCHANGE
selfhost
SENDCHANGE
sendvirtualinput
serverside
@@ -1879,6 +1880,7 @@ winexe
winforms
winget
wingetcreate
wingetpkgs
Winhook
WINL
winlogon

View File

@@ -123,7 +123,7 @@ jobs:
displayName: Stage UI Test Build Outputs
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: '$(BuildPlatform)/$(BuildConfiguration)/**/*'
contents: '**/$(BuildPlatform)/$(BuildConfiguration)/tests/**/*'
targetFolder: '$(JobOutputDirectory)\$(BuildPlatform)\$(BuildConfiguration)'
- publish: $(JobOutputDirectory)

View File

@@ -11,12 +11,14 @@ parameters:
- name: useLatestWebView2
type: boolean
default: false
- name: useLatestOfficialBuild
type: boolean
default: true
- name: useCurrentBranchBuild
type: boolean
default: false
- name: buildSource
type: string
default: "latestMainOfficialBuild"
displayName: "Build Source"
- name: specificBuildId
type: string
default: "xxxx"
displayName: "Build ID (for specific builds)"
- name: uiTestModules
type: object
default: []
@@ -113,16 +115,17 @@ jobs:
& '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1'
displayName: Download and install WinAppDriver
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'specific'
project: 'Dart'
definition: '76541'
buildVersionToDownload: 'latestFromBranch'
${{ if eq(parameters.useCurrentBranchBuild, true) }}:
branchName: '$(Build.SourceBranch)'
${{ if eq(parameters.buildSource, 'specificBuildId') }}:
buildVersionToDownload: 'specific'
buildId: '${{ parameters.specificBuildId }}'
${{ else }}:
buildVersionToDownload: 'latestFromBranch'
branchName: 'refs/heads/main'
artifactName: 'build-$(BuildPlatform)-Release'
targetPath: '$(Build.ArtifactStagingDirectory)'
@@ -133,7 +136,7 @@ jobs:
patterns: |
**/PowerToysSetup*.exe
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
- ${{ if eq(parameters.installMode, 'peruser') }}:
- pwsh: |-
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "PerUser"
@@ -169,7 +172,7 @@ jobs:
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
env:
platform: '$(TestPlatform)'
useInstallerForTest: ${{ parameters.useLatestOfficialBuild }}
useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }}
- ${{ if ne(length(parameters.uiTestModules), 0) }}:
- ${{ each module in parameters.uiTestModules }}:
@@ -191,4 +194,4 @@ jobs:
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
env:
platform: '$(TestPlatform)'
useInstallerForTest: ${{ parameters.useLatestOfficialBuild }}
useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }}

View File

@@ -19,155 +19,40 @@ parameters:
- name: useLatestWebView2
type: boolean
default: false
- name: useLatestOfficialBuild
type: boolean
default: true
- name: testBothInstallModes
type: boolean
default: true
- name: useCurrentBranchBuild
type: boolean
default: false
- name: buildSource
type: string
default: "latestMainOfficialBuild"
displayName: "Build Source"
values:
- latestMainOfficialBuild
- buildNow
- specificBuildId
- name: specificBuildId
type: string
default: 'xxxx'
displayName: "Build ID (only used when Build Source = specificBuildId)"
- name: uiTestModules
type: object
default: []
stages:
- ${{ each platform in parameters.buildPlatforms }}:
- ${{ if eq(parameters.useLatestOfficialBuild, false) }}:
- stage: Build_${{ platform }}
displayName: Build ${{ platform }}
dependsOn: []
jobs:
- template: job-build-project.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
buildPlatforms:
- ${{ platform }}
buildConfigurations: [Release]
enablePackageCaching: true
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: false
buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
timeoutInMinutes: 90
# Full build path: build PowerToys + UI tests + run tests
- ${{ if eq(parameters.buildSource, 'buildNow') }}:
- template: pipeline-ui-tests-full-build.yml
parameters:
platform: ${{ platform }}
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
uiTestModules: ${{ parameters.uiTestModules }}
- ${{ if eq(parameters.useLatestOfficialBuild, true) }}:
- stage: BuildUITests_${{ platform }}
displayName: Build UI Tests Only
dependsOn: []
jobs:
- template: job-build-ui-tests.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
buildPlatforms:
- ${{ platform }}
uiTestModules: ${{ parameters.uiTestModules }}
- ${{ if eq(platform, 'x64') }}:
- stage: Test_x64Win10
displayName: Test x64Win10
${{ if eq(parameters.useLatestOfficialBuild, true) }}:
dependsOn:
- BuildUITests_${{ platform }}
${{ else }}:
dependsOn:
- Build_${{ platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win10
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
uiTestModules: ${{ parameters.uiTestModules }}
# Additional per-user installation test (when both modes are enabled)
- ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}:
- template: job-test-project.yml
parameters:
platform: x64Win10
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
uiTestModules: ${{ parameters.uiTestModules }}
installMode: 'peruser'
jobSuffix: '_PerUser'
- ${{ if eq(platform, 'x64') }}:
- stage: Test_x64Win11
displayName: Test x64Win11
${{ if eq(parameters.useLatestOfficialBuild, true) }}:
dependsOn:
- BuildUITests_${{ platform }}
${{ else }}:
dependsOn:
- Build_${{ platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win11
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
uiTestModules: ${{ parameters.uiTestModules }}
# Additional per-user installation test (when both modes are enabled)
- ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}:
- template: job-test-project.yml
parameters:
platform: x64Win11
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
uiTestModules: ${{ parameters.uiTestModules }}
installMode: 'peruser'
jobSuffix: '_PerUser'
- ${{ if ne(platform, 'x64') }}:
- stage: Test_${{ platform }}
displayName: Test ${{ platform }}
${{ if eq(parameters.useLatestOfficialBuild, true) }}:
dependsOn:
- BuildUITests_${{ platform }}
${{ else }}:
dependsOn:
- Build_${{ platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: ${{ platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
uiTestModules: ${{ parameters.uiTestModules }}
# Additional per-user installation test (when both modes are enabled)
- ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}:
- template: job-test-project.yml
parameters:
platform: ${{ platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }}
useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }}
uiTestModules: ${{ parameters.uiTestModules }}
installMode: 'peruser'
jobSuffix: '_PerUser'
# Official build path: build UI tests only + download official build + run tests
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
- template: pipeline-ui-tests-official-build.yml
parameters:
platform: ${{ platform }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
uiTestModules: ${{ parameters.uiTestModules }}

View File

@@ -0,0 +1,80 @@
# Template for full build path: Build PowerToys + Build UI Tests + Run Tests
parameters:
- name: platform
type: string
- name: enableMsBuildCaching
type: boolean
default: false
- name: useVSPreview
type: boolean
default: false
- name: useLatestWebView2
type: boolean
default: false
- name: uiTestModules
type: object
default: []
stages:
# Stage 1: Build full PowerToys project
- stage: Build_${{ parameters.platform }}
displayName: Build PowerToys ${{ parameters.platform }}
dependsOn: []
jobs:
- template: job-build-project.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
buildPlatforms:
- ${{ parameters.platform }}
buildConfigurations: [Release]
enablePackageCaching: true
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: false
buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
timeoutInMinutes: 90
# Stage 2: Run UI Tests
- ${{ if eq(parameters.platform, 'x64') }}:
- stage: Test_x64Win10_FullBuild
displayName: Test x64Win10 (Full Build)
dependsOn: Build_${{ parameters.platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win10
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: 'buildNow'
uiTestModules: ${{ parameters.uiTestModules }}
- stage: Test_x64Win11_FullBuild
displayName: Test x64Win11 (Full Build)
dependsOn: Build_${{ parameters.platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win11
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: 'buildNow'
uiTestModules: ${{ parameters.uiTestModules }}
- ${{ if ne(parameters.platform, 'x64') }}:
- stage: Test_${{ parameters.platform }}_FullBuild
displayName: Test ${{ parameters.platform }} (Full Build)
dependsOn: Build_${{ parameters.platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: ${{ parameters.platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: 'buildNow'
uiTestModules: ${{ parameters.uiTestModules }}

View File

@@ -0,0 +1,110 @@
# Template for official build path: Download Official Build + Build UI Tests Only + Run Tests
parameters:
- name: platform
type: string
- name: buildSource
type: string
- name: specificBuildId
type: string
default: 'xxxx'
- name: useLatestWebView2
type: boolean
default: false
- name: uiTestModules
type: object
default: []
stages:
# Stage 1: Build UI Tests Only
- stage: BuildUITests_${{ parameters.platform }}
displayName: Build UI Tests Only ${{ parameters.platform }}
dependsOn: []
jobs:
- template: job-build-ui-tests.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
buildPlatforms:
- ${{ parameters.platform }}
uiTestModules: ${{ parameters.uiTestModules }}
# Stage 2: Run UI Tests with Official Build
- ${{ if eq(parameters.platform, 'x64') }}:
- stage: Test_x64Win10_OfficialBuild
displayName: Test x64Win10 (Official Build)
dependsOn: BuildUITests_${{ parameters.platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win10
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
uiTestModules: ${{ parameters.uiTestModules }}
# Additional per-user installation test
- template: job-test-project.yml
parameters:
platform: x64Win10
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
uiTestModules: ${{ parameters.uiTestModules }}
installMode: 'peruser'
jobSuffix: '_PerUser'
- stage: Test_x64Win11_OfficialBuild
displayName: Test x64Win11 (Official Build)
dependsOn: BuildUITests_${{ parameters.platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win11
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
uiTestModules: ${{ parameters.uiTestModules }}
# Additional per-user installation test
- template: job-test-project.yml
parameters:
platform: x64Win11
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
uiTestModules: ${{ parameters.uiTestModules }}
installMode: 'peruser'
jobSuffix: '_PerUser'
- ${{ if ne(parameters.platform, 'x64') }}:
- stage: Test_${{ parameters.platform }}_OfficialBuild
displayName: Test ${{ parameters.platform }} (Official Build)
dependsOn: BuildUITests_${{ parameters.platform }}
jobs:
- template: job-test-project.yml
parameters:
platform: ${{ parameters.platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
uiTestModules: ${{ parameters.uiTestModules }}
# Additional per-user installation test
- template: job-test-project.yml
parameters:
platform: ${{ parameters.platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
buildSource: ${{ parameters.buildSource }}
specificBuildId: ${{ parameters.specificBuildId }}
uiTestModules: ${{ parameters.uiTestModules }}
installMode: 'peruser'
jobSuffix: '_PerUser'

View File

@@ -34,22 +34,22 @@
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.8" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.7" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.7" />
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.8" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.7" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.8" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
<!--
@@ -78,28 +78,28 @@
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.CodeDom" Version="9.0.7" />
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.7" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.7" />
<PackageVersion Include="System.Data.OleDb" Version="9.0.7" />
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
<PackageVersion Include="System.Data.OleDb" Version="9.0.8" />
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.7" />
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.8" />
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.7" />
<PackageVersion Include="System.Drawing.Common" Version="9.0.7" />
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.8" />
<PackageVersion Include="System.Drawing.Common" Version="9.0.8" />
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageVersion Include="System.Management" Version="9.0.7" />
<PackageVersion Include="System.Management" Version="9.0.8" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.7" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.7" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.8" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.8" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.8" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" />

View File

@@ -1519,23 +1519,23 @@ SOFTWARE.
- Mages 3.0.0
- Markdig.Signed 0.34.0
- MessagePack 3.1.3
- Microsoft.Bcl.AsyncInterfaces 9.0.7
- Microsoft.Bcl.AsyncInterfaces 9.0.8
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
- Microsoft.Data.Sqlite 9.0.7
- Microsoft.Data.Sqlite 9.0.8
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
- Microsoft.DotNet.ILCompiler (A)
- Microsoft.Extensions.DependencyInjection 9.0.7
- Microsoft.Extensions.Hosting 9.0.7
- Microsoft.Extensions.Hosting.WindowsServices 9.0.7
- Microsoft.Extensions.Logging 9.0.7
- Microsoft.Extensions.Logging.Abstractions 9.0.7
- Microsoft.Extensions.DependencyInjection 9.0.8
- Microsoft.Extensions.Hosting 9.0.8
- Microsoft.Extensions.Hosting.WindowsServices 9.0.8
- Microsoft.Extensions.Logging 9.0.8
- Microsoft.Extensions.Logging.Abstractions 9.0.8
- Microsoft.NET.ILLink.Tasks (A)
- Microsoft.SemanticKernel 1.15.0
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
- Microsoft.Web.WebView2 1.0.2903.40
- Microsoft.Win32.SystemEvents 9.0.7
- Microsoft.Windows.Compatibility 9.0.7
- Microsoft.Win32.SystemEvents 9.0.8
- Microsoft.Windows.Compatibility 9.0.8
- Microsoft.Windows.CsWin32 0.3.183
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188
@@ -1555,25 +1555,25 @@ SOFTWARE.
- SkiaSharp.Views.WinUI 2.88.9
- StreamJsonRpc 2.21.69
- StyleCop.Analyzers 1.2.0-beta.556
- System.CodeDom 9.0.7
- System.CodeDom 9.0.8
- System.CommandLine 2.0.0-beta4.22272.1
- System.ComponentModel.Composition 9.0.7
- System.Configuration.ConfigurationManager 9.0.7
- System.Data.OleDb 9.0.7
- System.ComponentModel.Composition 9.0.8
- System.Configuration.ConfigurationManager 9.0.8
- System.Data.OleDb 9.0.8
- System.Data.SqlClient 4.9.0
- System.Diagnostics.EventLog 9.0.7
- System.Diagnostics.PerformanceCounter 9.0.7
- System.Drawing.Common 9.0.7
- System.Diagnostics.EventLog 9.0.8
- System.Diagnostics.PerformanceCounter 9.0.8
- System.Drawing.Common 9.0.8
- System.IO.Abstractions 22.0.13
- System.IO.Abstractions.TestingHelpers 22.0.13
- System.Management 9.0.7
- System.Management 9.0.8
- System.Net.Http 4.3.4
- System.Private.Uri 4.3.2
- System.Reactive 6.0.1
- System.Runtime.Caching 9.0.7
- System.ServiceProcess.ServiceController 9.0.7
- System.Text.Encoding.CodePages 9.0.7
- System.Text.Json 9.0.7
- System.Runtime.Caching 9.0.8
- System.ServiceProcess.ServiceController 9.0.8
- System.Text.Encoding.CodePages 9.0.8
- System.Text.Json 9.0.8
- System.Text.RegularExpressions 4.3.1
- UnicodeInformation 2.6.0
- UnitsNet 5.56.0

View File

@@ -461,6 +461,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.Common", "src\modules\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.FilePreviewer", "src\modules\peek\Peek.FilePreviewer\Peek.FilePreviewer.csproj", "{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Peek.UITests", "src\modules\peek\Peek.UITests\Peek.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MarkdownPreviewHandlerCpp", "src\modules\previewpane\MarkdownPreviewHandlerCpp\MarkdownPreviewHandlerCpp.vcxproj", "{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GcodePreviewHandlerCpp", "src\modules\previewpane\GcodePreviewHandlerCpp\GcodePreviewHandlerCpp.vcxproj", "{5A5DD09D-723A-44D3-8F2B-293584C3D731}"
@@ -1854,6 +1856,14 @@ Global
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|ARM64.Build.0 = Release|ARM64
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.ActiveCfg = Release|x64
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.Build.0 = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|x64.ActiveCfg = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|x64.Build.0 = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|ARM64.Build.0 = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|x64.ActiveCfg = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|x64.Build.0 = Release|x64
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|ARM64.ActiveCfg = Debug|ARM64
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|ARM64.Build.0 = Debug|ARM64
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|x64.ActiveCfg = Debug|x64
@@ -2998,6 +3008,7 @@ Global
{9D7A6DE0-7D27-424D-ABAE-41B2161F9A03} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
{17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
{ED9A1AC6-AEB0-4569-A6E9-E1696182B545} = {2F305555-C296-497E-AC20-5FA1B237996A}
{5A5DD09D-723A-44D3-8F2B-293584C3D731} = {2F305555-C296-497E-AC20-5FA1B237996A}
{B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9} = {2F305555-C296-497E-AC20-5FA1B237996A}

View File

@@ -22,23 +22,23 @@ The PowerToys UI test pipeline provides flexible options for building and testin
### Pipeline Options
- **useLatestOfficialBuild**: When checked, downloads the latest official PowerToys build and installs it for testing. This skips the full solution build and only builds UI test projects.
- **buildSource**: Select the build type for testing:
- `latestMainOfficialBuild`: Downloads and uses the latest official PowerToys build from main branch
- `buildNow`: Builds PowerToys from current source code and uses it for testing
- `specificBuildId`: Downloads a specific PowerToys build using the build ID specified in `specificBuildId` parameter
- **useCurrentBranchBuild**: When checked along with `useLatestOfficialBuild`, downloads the official build from the current branch instead of main.
**Default value**: `latestMainOfficialBuild`
**Default value**: `false` (downloads from main branch)
- **specificBuildId**: When `buildSource` is set to `specificBuildId`, specify the exact PowerToys build ID to download and test against.
**Default value**: `"xxxx"` (placeholder, enter actual build ID when using specificBuildId option)
**When to use this**:
- **Default scenario**: The pipeline tests against the latest signed PowerToys build from the `main` branch, regardless of which branch your test code changes are from
- **Custom branch testing**: Only specify `true` when:
- Your branch has produced its own signed PowerToys build via the official build pipeline
- You want to test against that specific branch's PowerToys build instead of main
- You are testing PowerToys functionality changes that are only available in your branch's build
- Testing against a specific known build for reproducibility
- Regression testing against a particular build version
- Validating fixes in a specific build before release
**Important notes**:
- The test pipeline itself runs from your specified branch, but by default tests against the main branch's PowerToys build
- Not all branches have signed builds available - only use this if you're certain your branch has a signed build
- If enabled but no build exists for your branch, the pipeline may fail or fall back to main
**Usage**: Enter the build ID number (e.g., `12345`) to download that specific build. Only used when `buildSource` is set to `specificBuildId`.
- **uiTestModules**: Specify which UI test modules to build and run. This parameter controls both the `.csproj` projects to build and the `.dll` test assemblies to execute. Examples:
- `['UITests-FancyZones']` - Only FancyZones UI tests
@@ -50,19 +50,19 @@ The PowerToys UI test pipeline provides flexible options for building and testin
### Build Modes
1. **Official Build + Selective Testing** (`useLatestOfficialBuild = true`)
- Downloads and installs official PowerToys build
- Builds only specified UI test projects
- Runs specified UI tests against installed PowerToys
- Controlled by `uiTestModules` parameter
1. **Official Build Testing** (`buildSource = latestMainOfficialBuild` or `specificBuildId`)
- Downloads and installs official PowerToys build (latest from main or specific build ID)
- Builds only UI test projects (all or specific based on `uiTestModules`)
- Runs UI tests against installed PowerToys
- Tests both machine-level and per-user installation modes automatically
2. **Full Build + Testing** (`useLatestOfficialBuild = false`)
- Builds entire PowerToys solution
2. **Current Source Build Testing** (`buildSource = buildNow`)
- Builds entire PowerToys solution from current source code
- Builds UI test projects (all or specific based on `uiTestModules`)
- Runs UI tests (all or specific based on `uiTestModules`)
- Uses freshly built PowerToys for testing
- Runs UI tests against freshly built PowerToys
- Uses artifacts from current pipeline build
> **Note**: Both modes support the `uiTestModules` parameter to control which specific UI test modules to build and run.
> **Note**: All modes support the `uiTestModules` parameter to control which specific UI test modules to build and run. Both machine-level and per-user installation modes are tested automatically when using official builds.
### Pipeline Access
- Pipeline: https://microsoft.visualstudio.com/Dart/_build?definitionId=161438&_a=summary

View File

@@ -87,6 +87,13 @@
### Building PowerToys Locally
#### One stop script for building installer
1. Open developer powershell for vs 2022
2. Run tools\build\build-installer.ps1
> For the first-time setup, please run the installer as an administrator. This ensures that the Wix tool can move wix.target to the desired location and trust the certificate used to sign the MSIX packages.
The following manual steps will not install the MSIX apps (such as Command Palette) on your local installer.
#### Prerequisites for building the MSI installer
1. Install the [WiX Toolset Visual Studio 2022 Extension](https://marketplace.visualstudio.com/items?itemName=WixToolset.WixToolsetVisualStudio2022Extension).

View File

@@ -0,0 +1,33 @@
## If for any reason, you'd like to test winget install scenario, you can follow this doc:
### Powertoys winget manifest definition:
[winget repository](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/PowerToys)
### How to test a winget installation locally:
1. Get artifacts from release CI pipeline Pipelines - Runs for PowerToys Signed YAML Release Build, or you can build one yourself by execute the
'tools\build\build-installer.ps1' script
2. Get the artifact hash, this is required to define winget manifest
```powershell
cd /path/to/your/directory/contains/installer
Get-FileHash -Path ".\<Installer-name>.exe" -Algorithm SHA256
```
3. Host your installer.exe - Attention: staged github release artifacts or artifacts in release pipeline is not OK in this step
You can self-host it or you can upload to a publicly available endpoint
**How to selfhost it** (A extremely simple way):
```powershell
python -m http.server 8000
```
4. Download a version folder from wingetpkgs like: [version 0.92.1](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/PowerToys/0.92.1)
and you get **a folder contains 3 yml files**
>note: Do not put any files other than these three in this folder
5. Modify the yml files based on your version and the self hosted artifact link, and modify the sha256 hash for the installer you'd like to use
6. Start winget install:
```powershell
#execute as admin
winget settings --enable LocalManifestFiles
winget install --manifest "<folder_path_of_manifest_files>" --architecture x64 --scope user
```

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
@@ -25,8 +27,9 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <param name="value">The text to set.</param>
/// <param name="clearText">A value indicating whether to clear the text before setting it. Default value is true</param>
/// <param name="charDelayMS">Delay in milliseconds between each character. Default is 0 (no delay).</param>
/// <returns>The current TextBox instance.</returns>
public TextBox SetText(string value, bool clearText = true)
public TextBox SetText(string value, bool clearText = true, int charDelayMS = 0)
{
if (clearText)
{
@@ -39,10 +42,36 @@ namespace Microsoft.PowerToys.UITest
Task.Delay(500).Wait();
}
PerformAction((actions, windowElement) =>
// TODO: CmdPal bug when inputting text, characters are swallowed too quickly.
// This should be fixed within CmdPal itself.
// Temporary workaround: introduce a delay between character inputs to avoid the issue
if (charDelayMS > 0 || EnvironmentConfig.IsInPipeline)
{
windowElement.SendKeys(value);
});
// Send text character by character with delay (if specified or in pipeline)
PerformAction((actions, windowElement) =>
{
foreach (char c in value)
{
windowElement.SendKeys(c.ToString());
if (charDelayMS > 0)
{
Task.Delay(charDelayMS).Wait();
}
else if (EnvironmentConfig.IsInPipeline)
{
Task.Delay(50).Wait();
}
}
});
}
else
{
// No character delay - send all text at once (original behavior)
PerformAction((actions, windowElement) =>
{
windowElement.SendKeys(value);
});
}
return this;
}

View File

@@ -0,0 +1,45 @@
// 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;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Centralized configuration for all environment variables used in UI tests.
/// </summary>
public static class EnvironmentConfig
{
private static readonly Lazy<bool> _isInPipeline = new(() =>
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform")));
private static readonly Lazy<bool> _useInstallerForTest = new(() =>
{
string? envValue = Environment.GetEnvironmentVariable("useInstallerForTest") ??
Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
return !string.IsNullOrEmpty(envValue) && bool.TryParse(envValue, out bool result) && result;
});
private static readonly Lazy<string?> _platform = new(() =>
Environment.GetEnvironmentVariable("platform"));
/// <summary>
/// Gets a value indicating whether the tests are running in a CI/CD pipeline.
/// Determined by the presence of the "platform" environment variable.
/// </summary>
public static bool IsInPipeline => _isInPipeline.Value;
/// <summary>
/// Gets a value indicating whether to use installer paths for testing.
/// Checks both "useInstallerForTest" and "USEINSTALLERFORTEST" environment variables.
/// </summary>
public static bool UseInstallerForTest => _useInstallerForTest.Value;
/// <summary>
/// Gets the platform name from the environment variable.
/// Typically used in CI/CD pipelines to identify the build platform.
/// </summary>
public static string? Platform => _platform.Value;
}
}

View File

@@ -92,9 +92,7 @@ namespace Microsoft.PowerToys.UITest
private ModuleConfigData()
{
// Check if we should use installer paths from environment variable
string? useInstallerForTestEnv =
Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result;
UseInstallerForTest = EnvironmentConfig.UseInstallerForTest;
// Module information including executable name, window name, and optional subdirectory
ModuleInfo = new Dictionary<PowerToysModule, ModuleInfo>

View File

@@ -5,6 +5,7 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -37,6 +38,9 @@ namespace Microsoft.PowerToys.UITest
private PowerToysModule scope;
private string[]? commandLineArgs;
/// <summary>
/// Gets a value indicating whether to use installer paths for testing.
/// </summary>
private bool UseInstallerForTest { get; }
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
@@ -45,9 +49,7 @@ namespace Microsoft.PowerToys.UITest
this.scope = scope;
this.commandLineArgs = commandLineArgs;
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
string? useInstallerForTestEnv =
Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result;
UseInstallerForTest = EnvironmentConfig.UseInstallerForTest;
this.locationPath = UseInstallerForTest ? string.Empty : Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
CheckWinAppDriverAndRoot();
@@ -136,6 +138,10 @@ namespace Microsoft.PowerToys.UITest
{
TryLaunchPowerToysSettings(opts);
}
else if (scope == PowerToysModule.CommandPalette && UseInstallerForTest)
{
TryLaunchCommandPalette(opts);
}
else
{
opts.AddAdditionalCapability("app", appPath);
@@ -163,48 +169,77 @@ namespace Microsoft.PowerToys.UITest
private void TryLaunchPowerToysSettings(AppiumOptions opts)
{
CheckWinAppDriverAndRoot();
var runnerProcessInfo = new ProcessStartInfo
try
{
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
Thread.Sleep(5000);
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
if (root != null)
{
const int maxRetries = 5;
const int delayMs = 5000;
var windowName = "PowerToys Settings";
for (int attempt = 1; attempt <= maxRetries; attempt++)
var runnerProcessInfo = new ProcessStartInfo
{
var settingsWindow = ApiHelper.FindDesktopWindowHandler(
[windowName, AdministratorPrefix + windowName]);
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
if (settingsWindow.Count > 0)
{
var hexHwnd = settingsWindow[0].HWnd.ToString("x");
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
return;
}
ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
if (attempt < maxRetries)
{
Thread.Sleep(delayMs);
}
else
{
throw new TimeoutException("Failed to find PowerToys Settings window after multiple attempts.");
}
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
}
}
private void TryLaunchCommandPalette(AppiumOptions opts)
{
try
{
// Exit any existing CmdPal UI process
ExitExeByName("Microsoft.CmdPal.UI");
var processStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c start shell:appsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App",
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
};
var process = Process.Start(processStartInfo);
process?.WaitForExit();
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to launch Command Palette: {ex.Message}", ex);
}
}
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
var window = ApiHelper.FindDesktopWindowHandler(
[windowName, AdministratorPrefix + windowName]);
if (window.Count > 0)
{
var hexHwnd = window[0].HWnd.ToString("x");
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
return;
}
if (attempt < maxRetries)
{
Thread.Sleep(delayMs);
}
else
{
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
}
}
}

View File

@@ -5,6 +5,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
@@ -20,6 +21,9 @@ namespace Microsoft.PowerToys.UITest
public required Session Session { get; set; }
/// <summary>
/// Gets a value indicating whether the tests are running in a CI/CD pipeline.
/// </summary>
public bool IsInPipeline { get; }
public string? ScreenshotDirectory { get; set; }
@@ -34,8 +38,8 @@ namespace Microsoft.PowerToys.UITest
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
{
this.IsInPipeline = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform"));
Console.WriteLine($"Running tests on platform: {Environment.GetEnvironmentVariable("platform")}");
this.IsInPipeline = EnvironmentConfig.IsInPipeline;
Console.WriteLine($"Running tests on platform: {EnvironmentConfig.Platform}");
if (IsInPipeline)
{
NativeMethods.ChangeDisplayResolution(1920, 1080);
@@ -56,6 +60,7 @@ namespace Microsoft.PowerToys.UITest
[TestInitialize]
public void TestInit()
{
KeyboardHelper.SendKeys(Key.Win, Key.M);
CloseOtherApplications();
if (IsInPipeline)
{
@@ -247,6 +252,174 @@ namespace Microsoft.PowerToys.UITest
return this.Session.Has<Element>(name, timeoutMS, global);
}
/// <summary>
/// Finds an element using partial name matching (contains).
/// Useful for finding windows with variable titles like "filename.txt - Notepad" or "filename - Notepad".
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="partialName">Part of the name to search for.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected T FindByPartialName<T>(string partialName, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return Session.Find<T>(By.XPath($"//*[contains(@Name, '{partialName}')]"), timeoutMS, global);
}
/// <summary>
/// Finds an element using partial name matching (contains).
/// </summary>
/// <param name="partialName">Part of the name to search for.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected Element FindByPartialName(string partialName, int timeoutMS = 5000, bool global = false)
{
return FindByPartialName<Element>(partialName, timeoutMS, global);
}
/// <summary>
/// Base method for finding elements by selector and filtering by name pattern.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="selector">The selector to find initial candidates.</param>
/// <param name="namePattern">Pattern to match against the Name attribute. Supports regex patterns.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="errorMessage">Custom error message when no element is found.</param>
/// <returns>The found element.</returns>
private T FindByNamePattern<T>(By selector, string namePattern, int timeoutMS = 5000, bool global = false, string? errorMessage = null)
where T : Element, new()
{
var elements = Session.FindAll<T>(selector, timeoutMS, global);
var regex = new Regex(namePattern, RegexOptions.IgnoreCase);
foreach (var element in elements)
{
var name = element.GetAttribute("Name");
if (!string.IsNullOrEmpty(name) && regex.IsMatch(name))
{
return element;
}
}
throw new NoSuchElementException(errorMessage ?? $"No element found matching pattern: {namePattern}");
}
/// <summary>
/// Finds an element using regular expression pattern matching.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="pattern">Regular expression pattern to match against the Name attribute.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected T FindByPattern<T>(string pattern, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return FindByNamePattern<T>(By.XPath("//*[@Name]"), pattern, timeoutMS, global, $"No element found matching pattern: {pattern}");
}
/// <summary>
/// Finds an element using regular expression pattern matching.
/// </summary>
/// <param name="pattern">Regular expression pattern to match against the Name attribute.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected Element FindByPattern(string pattern, int timeoutMS = 5000, bool global = false)
{
return FindByPattern<Element>(pattern, timeoutMS, global);
}
/// <summary>
/// Finds an element by ClassName only.
/// Returns the first element found with the specified ClassName.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected T FindByClassName<T>(string className, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return Session.Find<T>(By.ClassName(className), timeoutMS, global);
}
/// <summary>
/// Finds an element by ClassName only.
/// Returns the first element found with the specified ClassName.
/// </summary>
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected Element FindByClassName(string className, int timeoutMS = 5000, bool global = false)
{
return FindByClassName<Element>(className, timeoutMS, global);
}
/// <summary>
/// Finds an element by ClassName and matches its Name attribute using regex pattern matching.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
/// <param name="namePattern">Pattern to match against the Name attribute. Supports regex patterns.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected T FindByClassNameAndNamePattern<T>(string className, string namePattern, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return FindByNamePattern<T>(By.ClassName(className), namePattern, timeoutMS, global, $"No element with ClassName '{className}' found matching name pattern: {namePattern}");
}
/// <summary>
/// Finds an element by ClassName and matches its Name attribute using regex pattern matching.
/// </summary>
/// <param name="className">The ClassName to search for (e.g., "Notepad", "CabinetWClass").</param>
/// <param name="namePattern">Pattern to match against the Name attribute. Supports regex patterns.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found element.</returns>
protected Element FindByClassNameAndNamePattern(string className, string namePattern, int timeoutMS = 5000, bool global = false)
{
return FindByClassNameAndNamePattern<Element>(className, namePattern, timeoutMS, global);
}
/// <summary>
/// Finds a Notepad window regardless of whether the file extension is shown in the title.
/// Handles both "filename.txt - Notepad" and "filename - Notepad" formats.
/// Uses ClassName to efficiently find Notepad windows first, then matches the filename.
/// </summary>
/// <param name="baseFileName">The base filename without extension (e.g., "test" for "test.txt").</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found Notepad window element.</returns>
protected Element FindNotepadWindow(string baseFileName, int timeoutMS = 5000, bool global = false)
{
string pattern = $@"^{Regex.Escape(baseFileName)}(\.\w+)?(\s*-\s*|\s+)Notepad$";
return FindByClassNameAndNamePattern("Notepad", pattern, timeoutMS, global);
}
/// <summary>
/// Finds an Explorer window regardless of the folder or file name display format.
/// Handles various Explorer window title formats like "FolderName", "FileName", "FolderName - File Explorer", etc.
/// Uses ClassName to efficiently find Explorer windows first, then matches the folder or file name.
/// </summary>
/// <param name="folderName">The folder or file name to search for (e.g., "Documents", "Desktop", "test.txt").</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found Explorer window element.</returns>
protected Element FindExplorerWindow(string folderName, int timeoutMS = 5000, bool global = false)
{
string pattern = $@"^{Regex.Escape(folderName)}(\s*-\s*(File\s+Explorer|Windows\s+Explorer))?$";
return FindByClassNameAndNamePattern("CabinetWClass", pattern, timeoutMS, global);
}
/// <summary>
/// Finds an Explorer window by partial folder path.
/// Useful when the full path might be displayed in the title.
/// </summary>
/// <param name="partialPath">Part of the folder path to search for.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>The found Explorer window element.</returns>
protected Element FindExplorerByPartialPath(string partialPath, int timeoutMS = 5000, bool global = false)
{
return FindByPartialName(partialPath, timeoutMS, global);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll<T>(by, timeoutMS)

View File

@@ -27,10 +27,8 @@ namespace Microsoft.PowerToys.UITest
[RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")]
public static void AreEqual(TestContext? testContext, Element element, string scenarioSubname = "")
{
var pipelinePlatform = Environment.GetEnvironmentVariable("platform");
// Perform visual validation only in the pipeline
if (string.IsNullOrEmpty(pipelinePlatform))
if (!EnvironmentConfig.IsInPipeline)
{
Console.WriteLine("Skip visual validation in the local run.");
return;
@@ -55,11 +53,11 @@ namespace Microsoft.PowerToys.UITest
if (string.IsNullOrWhiteSpace(scenarioSubname))
{
scenarioSubname = string.Join("_", callerClassName, callerName, pipelinePlatform);
scenarioSubname = string.Join("_", callerClassName, callerName, EnvironmentConfig.Platform);
}
else
{
scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), pipelinePlatform);
scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), EnvironmentConfig.Platform);
}
var baselineImageResourceName = callerMethod!.DeclaringType!.Assembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault();

View File

@@ -5,6 +5,7 @@
#include "MouseHighlighter.h"
#include "trace.h"
#include <cmath>
#include <algorithm>
#ifdef COMPOSITION
namespace winrt
@@ -49,6 +50,9 @@ private:
void BringToFront();
HHOOK m_mouseHook = NULL;
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
// Helpers for spotlight overlay
float GetDpiScale() const;
void UpdateSpotlightMask(float cx, float cy, float radius, bool show);
static constexpr auto m_className = L"MouseHighlighter";
static constexpr auto m_windowTitle = L"PowerToys Mouse Highlighter";
@@ -67,7 +71,14 @@ private:
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
winrt::CompositionSpriteShape m_spotlightPointer{ nullptr };
// Spotlight overlay (mask with soft feathered edge)
winrt::SpriteVisual m_overlay{ nullptr };
winrt::CompositionMaskBrush m_spotlightMask{ nullptr };
winrt::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr };
winrt::CompositionColorBrush m_spotlightSource{ nullptr };
winrt::CompositionColorGradientStop m_maskStopCenter{ nullptr };
winrt::CompositionColorGradientStop m_maskStopInner{ nullptr };
winrt::CompositionColorGradientStop m_maskStopOuter{ nullptr };
bool m_leftPointerEnabled = true;
bool m_rightPointerEnabled = true;
@@ -123,6 +134,35 @@ bool Highlighter::CreateHighlighter()
m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f });
m_root.Children().InsertAtTop(m_shape);
// Create spotlight overlay (soft feather, DPI-aware)
m_overlay = m_compositor.CreateSpriteVisual();
m_overlay.RelativeSizeAdjustment({ 1.0f, 1.0f });
m_spotlightSource = m_compositor.CreateColorBrush(m_alwaysColor);
m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush();
m_spotlightMaskGradient.MappingMode(winrt::CompositionMappingMode::Absolute);
// Center region fully transparent
m_maskStopCenter = m_compositor.CreateColorGradientStop();
m_maskStopCenter.Offset(0.0f);
m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
// Inner edge of feather (still transparent)
m_maskStopInner = m_compositor.CreateColorGradientStop();
m_maskStopInner.Offset(0.995f); // will be updated per-radius
m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
// Outer edge (opaque mask -> overlay visible)
m_maskStopOuter = m_compositor.CreateColorGradientStop();
m_maskStopOuter.Offset(1.0f);
m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255));
m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter);
m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner);
m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter);
m_spotlightMask = m_compositor.CreateMaskBrush();
m_spotlightMask.Source(m_spotlightSource);
m_spotlightMask.Mask(m_spotlightMaskGradient);
m_overlay.Brush(m_spotlightMask);
m_overlay.IsVisible(false);
m_root.Children().InsertAtTop(m_overlay);
return true;
}
catch (...)
@@ -165,12 +205,8 @@ void Highlighter::AddDrawingPoint(MouseButton button)
// always
if (m_spotlightMode)
{
float borderThickness = static_cast<float>(std::hypot(GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN)));
circleGeometry.Radius({ static_cast<float>(borderThickness / 2.0 + m_radius), static_cast<float>(borderThickness / 2.0 + m_radius) });
circleShape.FillBrush(nullptr);
circleShape.StrokeBrush(m_compositor.CreateColorBrush(m_alwaysColor));
circleShape.StrokeThickness(borderThickness);
m_spotlightPointer = circleShape;
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
return;
}
else
{
@@ -209,20 +245,14 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
}
else
{
// always
// always / spotlight idle
if (m_spotlightMode)
{
if (m_spotlightPointer)
{
m_spotlightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
}
else
else if (m_alwaysPointer)
{
if (m_alwaysPointer)
{
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
}
}
@@ -266,9 +296,9 @@ void Highlighter::ClearDrawingPoint()
{
if (m_spotlightMode)
{
if (m_spotlightPointer)
if (m_overlay)
{
m_spotlightPointer.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
m_overlay.IsVisible(false);
}
}
else
@@ -421,7 +451,10 @@ void Highlighter::StopDrawing()
m_leftPointer = nullptr;
m_rightPointer = nullptr;
m_alwaysPointer = nullptr;
m_spotlightPointer = nullptr;
if (m_overlay)
{
m_overlay.IsVisible(false);
}
ShowWindow(m_hwnd, SW_HIDE);
UnhookWindowsHookEx(m_mouseHook);
ClearDrawing();
@@ -452,6 +485,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
m_rightPointerEnabled = false;
}
// Keep spotlight overlay color updated
if (m_spotlightSource)
{
m_spotlightSource.Color(m_alwaysColor);
}
if (!m_spotlightMode && m_overlay)
{
m_overlay.IsVisible(false);
}
if (instance->m_visible)
{
instance->StopDrawing();
@@ -563,6 +606,43 @@ void Highlighter::Terminate()
}
}
float Highlighter::GetDpiScale() const
{
return static_cast<float>(GetDpiForWindow(m_hwnd)) / 96.0f;
}
// Update spotlight radial mask center/radius with DPI-aware feather
void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool show)
{
if (!m_spotlightMaskGradient)
{
return;
}
m_spotlightMaskGradient.EllipseCenter({ cx, cy });
m_spotlightMaskGradient.EllipseRadius({ radius, radius });
const float dpiScale = GetDpiScale();
// Target a very fine edge: ~1 physical pixel, convert to DIPs: 1 / dpiScale
const float featherDip = 1.0f / (dpiScale > 0.0f ? dpiScale : 1.0f);
const float safeRadius = (std::max)(radius, 1.0f);
const float featherRel = (std::min)(0.25f, featherDip / safeRadius);
if (m_maskStopInner)
{
m_maskStopInner.Offset((std::max)(0.0f, 1.0f - featherRel));
}
if (m_spotlightSource)
{
m_spotlightSource.Color(m_alwaysColor);
}
if (m_overlay)
{
m_overlay.IsVisible(show);
}
}
#pragma region MouseHighlighter_API
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)

File diff suppressed because it is too large Load Diff

View File

@@ -1,284 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.Threading;
// <summary>
// Initialization and clean up.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
using Microsoft.Win32;
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
using MouseWithoutBorders.Form;
using Windows.UI.Input.Preview.Injection;
using Thread = MouseWithoutBorders.Core.Thread;
namespace MouseWithoutBorders
{
internal partial class Common
{
private static bool initDone;
internal static int REOPEN_WHEN_WSAECONNRESET = -10054;
internal static int REOPEN_WHEN_HOTKEY = -10055;
internal static int PleaseReopenSocket;
internal static bool ReopenSocketDueToReadError;
internal static DateTime LastResumeSuspendTime { get; set; } = DateTime.UtcNow;
internal static bool InitDone
{
get => Common.initDone;
set => Common.initDone = value;
}
internal static void UpdateMachineTimeAndID()
{
Common.MachineName = Common.MachineName.Trim();
_ = MachineStuff.MachinePool.TryUpdateMachineID(Common.MachineName, Common.MachineID, true);
}
private static void InitializeMachinePoolFromSettings()
{
try
{
MachineInf[] info = MachinePoolHelpers.LoadMachineInfoFromMachinePoolStringSetting(Setting.Values.MachinePoolString);
for (int i = 0; i < info.Length; i++)
{
info[i].Name = info[i].Name.Trim();
}
MachineStuff.MachinePool.Initialize(info);
MachineStuff.MachinePool.ResetIPAddressesForDeadMachines(true);
}
catch (Exception ex)
{
Logger.Log(ex);
MachineStuff.MachinePool.Clear();
}
}
internal static void SetupMachineNameAndID()
{
try
{
GetMachineName();
DesMachineID = MachineStuff.NewDesMachineID = MachineID;
// MessageBox.Show(machineID.ToString(CultureInfo.CurrentCulture)); // For test
InitializeMachinePoolFromSettings();
Common.MachineName = Common.MachineName.Trim();
_ = MachineStuff.MachinePool.LearnMachine(Common.MachineName);
_ = MachineStuff.MachinePool.TryUpdateMachineID(Common.MachineName, Common.MachineID, true);
MachineStuff.UpdateMachinePoolStringSetting();
}
catch (Exception e)
{
Logger.Log(e);
}
}
internal static void Init()
{
_ = Helper.GetUserName();
Common.GeneratedKey = true;
try
{
Common.MyKey = Setting.Values.MyKey;
int tmp = Setting.Values.MyKeyDaysToExpire;
}
catch (FormatException e)
{
Common.KeyCorrupted = true;
Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey();
Logger.Log(e.Message);
}
catch (CryptographicException e)
{
Common.KeyCorrupted = true;
Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey();
Logger.Log(e.Message);
}
try
{
InputSimulation.Injector = InputInjector.TryCreate();
if (InputSimulation.Injector != null)
{
InputSimulation.MoveMouseRelative(0, 0);
NativeMethods.InjectMouseInputAvailable = true;
}
}
catch (EntryPointNotFoundException)
{
NativeMethods.InjectMouseInputAvailable = false;
Logger.Log($"{nameof(NativeMethods.InjectMouseInputAvailable)} = false");
}
bool dummy = Setting.Values.DrawMouseEx;
Is64bitOS = IntPtr.Size == 8;
tcpPort = Setting.Values.TcpPort;
GetScreenConfig();
PackageSent = new PackageMonitor(0);
PackageReceived = new PackageMonitor(0);
SetupMachineNameAndID();
InitEncryption();
CreateHelperThreads();
SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);
NetworkChange.NetworkAvailabilityChanged += new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged);
SystemEvents.PowerModeChanged += new PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
PleaseReopenSocket = 9;
/* TODO: Telemetry for the matrix? */
}
private static void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
Helper.WndProcCounter++;
if (e.Mode is PowerModes.Resume or PowerModes.Suspend)
{
Logger.TelemetryLogTrace($"{nameof(SystemEvents_PowerModeChanged)}: {e.Mode}", SeverityLevel.Information);
LastResumeSuspendTime = DateTime.UtcNow;
MachineStuff.SwitchToMultipleMode(false, true);
}
}
private static void CreateHelperThreads()
{
// NOTE(@yuyoyuppe): service crashes while trying to obtain this info, disabling.
/*
Thread watchDogThread = new(new ThreadStart(WatchDogThread), nameof(WatchDogThread));
watchDogThread.Priority = ThreadPriority.Highest;
watchDogThread.Start();
*/
helper = new Thread(new ThreadStart(Helper.HelperThread), "Helper Thread");
helper.SetApartmentState(ApartmentState.STA);
helper.Start();
}
private static void AskHelperThreadsToExit(int waitTime)
{
Helper.signalHelperToExit = true;
Helper.signalWatchDogToExit = true;
_ = EvSwitch.Set();
int c = 0;
if (helper != null && c < waitTime)
{
while (Helper.signalHelperToExit)
{
Thread.Sleep(1);
}
helper = null;
}
}
internal static void Cleanup()
{
try
{
SendByeBye();
// UnhookClipboard();
AskHelperThreadsToExit(500);
MainForm.NotifyIcon.Visible = false;
MainForm.NotifyIcon.Dispose();
CloseAllFormsAndHooks();
DoSomethingInUIThread(() =>
{
Sk?.Close(true);
});
}
catch (Exception e)
{
Logger.Log(e);
}
}
private static long lastReleaseAllKeysCall;
internal static void ReleaseAllKeys()
{
if (Math.Abs(GetTick() - lastReleaseAllKeysCall) < 2000)
{
return;
}
lastReleaseAllKeysCall = GetTick();
KEYBDDATA kd;
kd.dwFlags = (int)LLKHF.UP;
VK[] keys = new VK[]
{
VK.LSHIFT, VK.LCONTROL, VK.LMENU, VK.LWIN, VK.RSHIFT,
VK.RCONTROL, VK.RMENU, VK.RWIN, VK.SHIFT, VK.MENU, VK.CONTROL,
};
Logger.LogDebug("***** ReleaseAllKeys has been called! *****:");
foreach (VK vk in keys)
{
if ((NativeMethods.GetAsyncKeyState((IntPtr)vk) & 0x8000) != 0)
{
Logger.LogDebug(vk.ToString() + " is down, release it...");
Hook?.ResetLastSwitchKeys(); // Sticky key can turn ALL PC mode on (CtrlCtrlCtrl)
kd.wVk = (int)vk;
InputSimulation.SendKey(kd);
Hook?.ResetLastSwitchKeys();
}
}
}
private static void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
Logger.LogDebug("NetworkAvailabilityEventArgs.IsAvailable: " + e.IsAvailable.ToString(CultureInfo.InvariantCulture));
Helper.WndProcCounter++;
ScheduleReopenSocketsDueToNetworkChanges(!e.IsAvailable);
}
private static void ScheduleReopenSocketsDueToNetworkChanges(bool closeSockets = true)
{
if (closeSockets)
{
// Slept/hibernated machine may still have the sockets' status as Connected:( (unchanged) so it would not re-connect after a timeout when waking up.
// Closing the sockets when it is going to sleep/hibernate will trigger the reconnection faster when it wakes up.
DoSomethingInUIThread(
() =>
{
SocketStuff s = Sk;
Sk = null;
s?.Close(false);
},
true);
}
if (!Common.IsMyDesktopActive())
{
PleaseReopenSocket = 0;
}
else if (PleaseReopenSocket != 10)
{
PleaseReopenSocket = 10;
}
}
}
}

View File

@@ -36,7 +36,7 @@ namespace MouseWithoutBorders
internal static string ActiveDesktop => Common.activeDesktop;
private static void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
internal static void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
{
GetScreenConfig();
}
@@ -340,7 +340,7 @@ namespace MouseWithoutBorders
Setting.Values.LastX = JUST_GOT_BACK_FROM_SCREEN_SAVER;
if (cleanupIfExit)
{
Common.Cleanup();
InitAndCleanup.Cleanup();
}
Process.GetCurrentProcess().KillProcess();

View File

@@ -32,6 +32,7 @@ using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
using MouseWithoutBorders.Exceptions;
using Clipboard = MouseWithoutBorders.Core.Clipboard;
using Thread = MouseWithoutBorders.Core.Thread;
// Log is enough
@@ -89,8 +90,8 @@ namespace MouseWithoutBorders
private static FrmMatrix matrixForm;
private static FrmInputCallback inputCallbackForm;
private static FrmAbout aboutForm;
private static Thread helper;
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
internal static Thread helper;
internal static int screenWidth;
internal static int screenHeight;
#pragma warning restore SA1307
@@ -120,7 +121,9 @@ namespace MouseWithoutBorders
internal static int switchCount;
#pragma warning restore SA1307
private static long lastReconnectByHotKeyTime;
private static int tcpPort;
#pragma warning disable SA1307 // Accessible fields should begin with upper-case names
internal static int tcpPort;
#pragma warning restore SA1307
private static bool secondOpenSocketTry;
private static string binaryName;
@@ -209,7 +212,7 @@ namespace MouseWithoutBorders
internal static bool Is64bitOS
{
get; private set;
get; set;
// set { Common.is64bitOS = value; }
}
@@ -610,7 +613,7 @@ namespace MouseWithoutBorders
}
* */
private static void SendByeBye()
internal static void SendByeBye()
{
Logger.LogDebug($"{nameof(SendByeBye)}");
SendPackage(ID.ALL, PackageType.ByeBye);
@@ -724,7 +727,7 @@ namespace MouseWithoutBorders
internal static void SendImage(string machine, string file)
{
LastDragDropFile = file;
Clipboard.LastDragDropFile = file;
// Send ClipboardCapture
if (machine.Equals("All", StringComparison.OrdinalIgnoreCase))
@@ -743,7 +746,7 @@ namespace MouseWithoutBorders
internal static void SendImage(ID src, string file)
{
LastDragDropFile = file;
Clipboard.LastDragDropFile = file;
// Send ClipboardCapture
SendPackage(src, PackageType.ClipboardCapture);
@@ -1290,7 +1293,7 @@ namespace MouseWithoutBorders
});
}
private static string GetMyStorageDir()
internal static string GetMyStorageDir()
{
string st = string.Empty;

View File

@@ -28,6 +28,7 @@ using MouseWithoutBorders.Core;
using SystemClipboard = System.Windows.Forms.Clipboard;
#if !MM_HELPER
using Clipboard = MouseWithoutBorders.Core.Clipboard;
using Thread = MouseWithoutBorders.Core.Thread;
#endif
@@ -159,7 +160,7 @@ namespace MouseWithoutBorders
public void SendClipboardData(ByteArrayOrString data, bool isFilePath)
{
_ = Common.CheckClipboardEx(data, isFilePath);
_ = Clipboard.CheckClipboardEx(data, isFilePath);
}
}
#endif

View File

@@ -579,7 +579,7 @@ namespace MouseWithoutBorders.Class
{
Common.ShowToolTip("Reconnecting...", 2000);
Common.LastReconnectByHotKeyTime = Common.GetTick();
Common.PleaseReopenSocket = Common.REOPEN_WHEN_HOTKEY;
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_HOTKEY;
return false;
}
@@ -632,7 +632,7 @@ namespace MouseWithoutBorders.Class
{
// Common.DoSomethingInUIThread(delegate()
{
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
}
// );

View File

@@ -407,7 +407,7 @@ namespace MouseWithoutBorders.Class
{
ResetModifiersState(Setting.Values.HotKeyLockMachine);
eatKey = true;
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
_ = NativeMethods.LockWorkStation();
}
}
@@ -439,7 +439,7 @@ namespace MouseWithoutBorders.Class
{
ctrlDown = altDown = false;
eatKey = true;
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
}
break;
@@ -449,7 +449,7 @@ namespace MouseWithoutBorders.Class
{
winDown = false;
eatKey = true;
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
uint rv = NativeMethods.LockWorkStation();
Logger.LogDebug("LockWorkStation returned " + rv.ToString(CultureInfo.CurrentCulture));
}

View File

@@ -235,7 +235,7 @@ namespace MouseWithoutBorders.Class
_ = Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.SetCompatibleTextRenderingDefault(false);
Common.Init();
InitAndCleanup.Init();
Core.Helper.WndProcCounter++;
var formScreen = new FrmScreen();
@@ -314,7 +314,7 @@ namespace MouseWithoutBorders.Class
MachineStuff.UpdateMachinePoolStringSetting();
SocketStuff.InvalidKeyFound = false;
Common.ReopenSocketDueToReadError = true;
InitAndCleanup.ReopenSocketDueToReadError = true;
Common.ReopenSockets(true);
MachineStuff.SendMachineMatrix();
@@ -340,7 +340,7 @@ namespace MouseWithoutBorders.Class
public void Reconnect()
{
SocketStuff.InvalidKeyFound = false;
Common.ReopenSocketDueToReadError = true;
InitAndCleanup.ReopenSocketDueToReadError = true;
Common.ReopenSockets(true);
for (int i = 0; i < 10; i++)
@@ -397,7 +397,7 @@ namespace MouseWithoutBorders.Class
using var asyncFlowControl = ExecutionContext.SuppressFlow();
Common.InputCallbackThreadID = Thread.CurrentThread.ManagedThreadId;
while (!Common.InitDone)
while (!InitAndCleanup.InitDone)
{
Thread.Sleep(100);
}

View File

@@ -118,7 +118,7 @@ namespace MouseWithoutBorders.Class
if (shouldReopenSockets)
{
SocketStuff.InvalidKeyFound = false;
Common.ReopenSocketDueToReadError = true;
InitAndCleanup.ReopenSocketDueToReadError = true;
Common.ReopenSockets(true);
}

View File

@@ -29,6 +29,7 @@ using MouseWithoutBorders.Core;
// </history>
using MouseWithoutBorders.Exceptions;
using Clipboard = MouseWithoutBorders.Core.Clipboard;
using Thread = MouseWithoutBorders.Core.Thread;
[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "MouseWithoutBorders.SocketStuff.#SendData(System.Byte[])", Justification = "Dotnet port with style preservation")]
@@ -281,7 +282,7 @@ namespace MouseWithoutBorders.Class
* */
Common.GetMachineName(); // IPs might have been changed
Common.UpdateMachineTimeAndID();
InitAndCleanup.UpdateMachineTimeAndID();
Logger.LogDebug("Creating sockets...");
@@ -308,7 +309,7 @@ namespace MouseWithoutBorders.Class
{
Logger.TelemetryLogTrace("Restarting the service dues to WSAEADDRINUSE.", SeverityLevel.Warning);
Program.StartService();
Common.PleaseReopenSocket = Common.REOPEN_WHEN_WSAECONNRESET;
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_WSAECONNRESET;
}
break;
@@ -1248,7 +1249,7 @@ namespace MouseWithoutBorders.Class
// WSAECONNRESET
if (e is ExpectedSocketException se && se.ShouldReconnect)
{
Common.PleaseReopenSocket = Common.REOPEN_WHEN_WSAECONNRESET;
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_WSAECONNRESET;
Logger.Log($"MainTCPRoutine: {nameof(FlagReopenSocketIfNeeded)}");
}
}
@@ -1306,7 +1307,7 @@ namespace MouseWithoutBorders.Class
}
catch (ObjectDisposedException e)
{
Common.PleaseReopenSocket = Common.REOPEN_WHEN_WSAECONNRESET;
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_WSAECONNRESET;
UpdateTcpSockets(currentTcp, SocketStatus.ForceClosed);
currentSocket.Close();
Logger.Log($"{nameof(MainTCPRoutine)}: The socket could have been closed/disposed by other threads: {e.Message}");
@@ -1353,10 +1354,10 @@ namespace MouseWithoutBorders.Class
* In this case, we should give ONE try to reconnect.
*/
if (Common.ReopenSocketDueToReadError)
if (InitAndCleanup.ReopenSocketDueToReadError)
{
Common.PleaseReopenSocket = Common.REOPEN_WHEN_WSAECONNRESET;
Common.ReopenSocketDueToReadError = false;
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_WSAECONNRESET;
InitAndCleanup.ReopenSocketDueToReadError = false;
}
break;
@@ -1641,7 +1642,7 @@ namespace MouseWithoutBorders.Class
bool clientPushData = true;
ClipboardPostAction postAction = ClipboardPostAction.Other;
bool handShaken = Common.ShakeHand(ref remoteEndPoint, s, out Stream enStream, out Stream deStream, ref clientPushData, ref postAction);
bool handShaken = Clipboard.ShakeHand(ref remoteEndPoint, s, out Stream enStream, out Stream deStream, ref clientPushData, ref postAction);
if (!handShaken)
{
@@ -1656,7 +1657,7 @@ namespace MouseWithoutBorders.Class
if (clientPushData)
{
Common.ReceiveAndProcessClipboardData(remoteEndPoint, s, enStream, deStream, $"{postAction}");
Clipboard.ReceiveAndProcessClipboardData(remoteEndPoint, s, enStream, deStream, $"{postAction}");
}
else
{
@@ -1680,23 +1681,23 @@ namespace MouseWithoutBorders.Class
const int CLOSE_TIMEOUT = 10;
byte[] header = new byte[1024];
string headerString = string.Empty;
if (Common.LastDragDropFile != null)
if (Clipboard.LastDragDropFile != null)
{
string fileName = null;
if (!Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
if (!File.Exists(Common.LastDragDropFile))
if (!File.Exists(Clipboard.LastDragDropFile))
{
headerString = Directory.Exists(Common.LastDragDropFile)
? $"{0}*{Common.LastDragDropFile} - Folder is not supported, zip it first!"
: Common.LastDragDropFile.Contains("- File too big")
? $"{0}*{Common.LastDragDropFile}"
: $"{0}*{Common.LastDragDropFile} not found!";
headerString = Directory.Exists(Clipboard.LastDragDropFile)
? $"{0}*{Clipboard.LastDragDropFile} - Folder is not supported, zip it first!"
: Clipboard.LastDragDropFile.Contains("- File too big")
? $"{0}*{Clipboard.LastDragDropFile}"
: $"{0}*{Clipboard.LastDragDropFile} not found!";
}
else
{
fileName = Common.LastDragDropFile;
fileName = Clipboard.LastDragDropFile;
headerString = $"{new FileInfo(fileName).Length}*{fileName}";
}
}))
@@ -1739,11 +1740,11 @@ namespace MouseWithoutBorders.Class
Logger.Log(log);
}
}
else if (!Common.IsClipboardDataImage && Common.LastClipboardData != null)
else if (!Clipboard.IsClipboardDataImage && Clipboard.LastClipboardData != null)
{
try
{
byte[] data = Common.LastClipboardData;
byte[] data = Clipboard.LastClipboardData;
headerString = $"{data.Length}*{"text"}";
Common.GetBytesU(headerString).CopyTo(header, 0);
@@ -1773,9 +1774,9 @@ namespace MouseWithoutBorders.Class
Logger.Log(log);
}
}
else if (Common.LastClipboardData != null && Common.LastClipboardData.Length > 0)
else if (Clipboard.LastClipboardData != null && Clipboard.LastClipboardData.Length > 0)
{
byte[] data = Common.LastClipboardData;
byte[] data = Clipboard.LastClipboardData;
headerString = $"{data.Length}*{"image"}";
Common.GetBytesU(headerString).CopyTo(header, 0);
@@ -1984,8 +1985,8 @@ namespace MouseWithoutBorders.Class
{
tcp = null;
Setting.Values.MachineId = Common.Ran.Next();
Common.UpdateMachineTimeAndID();
Common.PleaseReopenSocket = Common.REOPEN_WHEN_HOTKEY;
InitAndCleanup.UpdateMachineTimeAndID();
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_HOTKEY;
Logger.TelemetryLogTrace("MachineID conflict.", SeverityLevel.Information);
}

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ internal static class DragDrop
if (wParam == Common.WM_RBUTTONUP && IsDropping)
{
IsDropping = false;
Common.LastIDWithClipboardData = ID.NONE;
Clipboard.LastIDWithClipboardData = ID.NONE;
}
}
@@ -193,7 +193,7 @@ internal static class DragDrop
{
if (!string.IsNullOrEmpty(dragFileName) && (File.Exists(dragFileName) || Directory.Exists(dragFileName)))
{
Common.LastDragDropFile = dragFileName;
Clipboard.LastDragDropFile = dragFileName;
/*
* possibleDropMachineID is used as desID sent in DragDropStep06();
* */
@@ -270,7 +270,7 @@ internal static class DragDrop
else
{
IsDragging = false;
Common.LastIDWithClipboardData = ID.NONE;
Clipboard.LastIDWithClipboardData = ID.NONE;
}
}
}
@@ -280,7 +280,7 @@ internal static class DragDrop
Logger.LogDebug("DragDropStep10: Hide the form and get data...");
IsDropping = false;
IsDragging = false;
Common.LastIDWithClipboardData = ID.NONE;
Clipboard.LastIDWithClipboardData = ID.NONE;
Common.DoSomethingInUIThread(() =>
{
@@ -288,7 +288,7 @@ internal static class DragDrop
});
PowerToysTelemetry.Log.WriteEvent(new MouseWithoutBorders.Telemetry.MouseWithoutBordersDragAndDropEvent());
Common.GetRemoteClipboard("desktop");
Clipboard.GetRemoteClipboard("desktop");
}
internal static void DragDropStep11()
@@ -298,8 +298,8 @@ internal static class DragDrop
IsDropping = false;
IsDragging = false;
DragMachine = (ID)1;
Common.LastIDWithClipboardData = ID.NONE;
Common.LastDragDropFile = null;
Clipboard.LastIDWithClipboardData = ID.NONE;
Clipboard.LastDragDropFile = null;
MouseDown = false;
}
@@ -307,7 +307,7 @@ internal static class DragDrop
{
Logger.LogDebug("DragDropStep12: ClipboardDragDropEnd received");
IsDropping = false;
Common.LastIDWithClipboardData = ID.NONE;
Clipboard.LastIDWithClipboardData = ID.NONE;
Common.DoSomethingInUIThread(() =>
{

View File

@@ -74,7 +74,7 @@ internal static class Event
if (!p.IsEmpty)
{
Common.HasSwitchedMachineSinceLastCopy = true;
Clipboard.HasSwitchedMachineSinceLastCopy = true;
Logger.LogDebug(string.Format(
CultureInfo.CurrentCulture,
@@ -213,10 +213,10 @@ internal static class Event
if (MachineStuff.desMachineID == Common.MachineID)
{
if (Common.GetTick() - Common.clipboardCopiedTime < Common.BIG_CLIPBOARD_DATA_TIMEOUT)
if (Common.GetTick() - Clipboard.clipboardCopiedTime < Clipboard.BIG_CLIPBOARD_DATA_TIMEOUT)
{
Common.clipboardCopiedTime = 0;
Common.GetRemoteClipboard("PrepareToSwitchToMachine");
Clipboard.clipboardCopiedTime = 0;
Clipboard.GetRemoteClipboard("PrepareToSwitchToMachine");
}
}
else

View File

@@ -119,7 +119,7 @@ internal static class Helper
if (MachineStuff.NewDesMachineID == Common.MachineID)
{
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
}
}
}
@@ -317,7 +317,7 @@ internal static class Helper
Common.GetInputDesktop(),
0);
Common.HasSwitchedMachineSinceLastCopy = true;
Clipboard.HasSwitchedMachineSinceLastCopy = true;
// Common.CreateLowIntegrityProcess("\"" + Path.GetDirectoryName(Application.ExecutablePath) + "\\MouseWithoutBordersHelper.exe\"", string.Empty, 0, false, 0);
var processes = Process.GetProcessesByName(HelperProcessName);

View File

@@ -0,0 +1,278 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.Threading;
using Microsoft.Win32;
using MouseWithoutBorders.Class;
using Windows.UI.Input.Preview.Injection;
// <summary>
// Initialization and clean up.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
namespace MouseWithoutBorders.Core;
internal static class InitAndCleanup
{
private static bool initDone;
internal static int REOPEN_WHEN_WSAECONNRESET = -10054;
internal static int REOPEN_WHEN_HOTKEY = -10055;
internal static int PleaseReopenSocket;
internal static bool ReopenSocketDueToReadError;
private static DateTime LastResumeSuspendTime { get; set; } = DateTime.UtcNow;
internal static bool InitDone
{
get => InitAndCleanup.initDone;
set => InitAndCleanup.initDone = value;
}
internal static void UpdateMachineTimeAndID()
{
Common.MachineName = Common.MachineName.Trim();
_ = MachineStuff.MachinePool.TryUpdateMachineID(Common.MachineName, Common.MachineID, true);
}
private static void InitializeMachinePoolFromSettings()
{
try
{
MachineInf[] info = MachinePoolHelpers.LoadMachineInfoFromMachinePoolStringSetting(Setting.Values.MachinePoolString);
for (int i = 0; i < info.Length; i++)
{
info[i].Name = info[i].Name.Trim();
}
MachineStuff.MachinePool.Initialize(info);
MachineStuff.MachinePool.ResetIPAddressesForDeadMachines(true);
}
catch (Exception ex)
{
Logger.Log(ex);
MachineStuff.MachinePool.Clear();
}
}
private static void SetupMachineNameAndID()
{
try
{
Common.GetMachineName();
Common.DesMachineID = MachineStuff.NewDesMachineID = Common.MachineID;
// MessageBox.Show(machineID.ToString(CultureInfo.CurrentCulture)); // For test
InitializeMachinePoolFromSettings();
Common.MachineName = Common.MachineName.Trim();
_ = MachineStuff.MachinePool.LearnMachine(Common.MachineName);
_ = MachineStuff.MachinePool.TryUpdateMachineID(Common.MachineName, Common.MachineID, true);
MachineStuff.UpdateMachinePoolStringSetting();
}
catch (Exception e)
{
Logger.Log(e);
}
}
internal static void Init()
{
_ = Helper.GetUserName();
Common.GeneratedKey = true;
try
{
Common.MyKey = Setting.Values.MyKey;
int tmp = Setting.Values.MyKeyDaysToExpire;
}
catch (FormatException e)
{
Common.KeyCorrupted = true;
Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey();
Logger.Log(e.Message);
}
catch (CryptographicException e)
{
Common.KeyCorrupted = true;
Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey();
Logger.Log(e.Message);
}
try
{
InputSimulation.Injector = InputInjector.TryCreate();
if (InputSimulation.Injector != null)
{
InputSimulation.MoveMouseRelative(0, 0);
NativeMethods.InjectMouseInputAvailable = true;
}
}
catch (EntryPointNotFoundException)
{
NativeMethods.InjectMouseInputAvailable = false;
Logger.Log($"{nameof(NativeMethods.InjectMouseInputAvailable)} = false");
}
bool dummy = Setting.Values.DrawMouseEx;
Common.Is64bitOS = IntPtr.Size == 8;
Common.tcpPort = Setting.Values.TcpPort;
Common.GetScreenConfig();
Common.PackageSent = new PackageMonitor(0);
Common.PackageReceived = new PackageMonitor(0);
SetupMachineNameAndID();
Common.InitEncryption();
CreateHelperThreads();
SystemEvents.DisplaySettingsChanged += new EventHandler(Common.SystemEvents_DisplaySettingsChanged);
NetworkChange.NetworkAvailabilityChanged += new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged);
SystemEvents.PowerModeChanged += new PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
PleaseReopenSocket = 9;
/* TODO: Telemetry for the matrix? */
}
private static void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
Helper.WndProcCounter++;
if (e.Mode is PowerModes.Resume or PowerModes.Suspend)
{
Logger.TelemetryLogTrace($"{nameof(SystemEvents_PowerModeChanged)}: {e.Mode}", SeverityLevel.Information);
LastResumeSuspendTime = DateTime.UtcNow;
MachineStuff.SwitchToMultipleMode(false, true);
}
}
private static void CreateHelperThreads()
{
// NOTE(@yuyoyuppe): service crashes while trying to obtain this info, disabling.
/*
Thread watchDogThread = new(new ThreadStart(WatchDogThread), nameof(WatchDogThread));
watchDogThread.Priority = ThreadPriority.Highest;
watchDogThread.Start();
*/
Common.helper = new Thread(new ThreadStart(Helper.HelperThread), "Helper Thread");
Common.helper.SetApartmentState(ApartmentState.STA);
Common.helper.Start();
}
private static void AskHelperThreadsToExit(int waitTime)
{
Helper.signalHelperToExit = true;
Helper.signalWatchDogToExit = true;
_ = Common.EvSwitch.Set();
int c = 0;
if (Common.helper != null && c < waitTime)
{
while (Helper.signalHelperToExit)
{
Thread.Sleep(1);
}
Common.helper = null;
}
}
internal static void Cleanup()
{
try
{
Common.SendByeBye();
// UnhookClipboard();
AskHelperThreadsToExit(500);
Common.MainForm.NotifyIcon.Visible = false;
Common.MainForm.NotifyIcon.Dispose();
Common.CloseAllFormsAndHooks();
Common.DoSomethingInUIThread(() =>
{
Common.Sk?.Close(true);
});
}
catch (Exception e)
{
Logger.Log(e);
}
}
private static long lastReleaseAllKeysCall;
internal static void ReleaseAllKeys()
{
if (Math.Abs(Common.GetTick() - lastReleaseAllKeysCall) < 2000)
{
return;
}
lastReleaseAllKeysCall = Common.GetTick();
KEYBDDATA kd;
kd.dwFlags = (int)Common.LLKHF.UP;
VK[] keys = new VK[]
{
VK.LSHIFT, VK.LCONTROL, VK.LMENU, VK.LWIN, VK.RSHIFT,
VK.RCONTROL, VK.RMENU, VK.RWIN, VK.SHIFT, VK.MENU, VK.CONTROL,
};
Logger.LogDebug("***** ReleaseAllKeys has been called! *****:");
foreach (VK vk in keys)
{
if ((NativeMethods.GetAsyncKeyState((IntPtr)vk) & 0x8000) != 0)
{
Logger.LogDebug(vk.ToString() + " is down, release it...");
Common.Hook?.ResetLastSwitchKeys(); // Sticky key can turn ALL PC mode on (CtrlCtrlCtrl)
kd.wVk = (int)vk;
InputSimulation.SendKey(kd);
Common.Hook?.ResetLastSwitchKeys();
}
}
}
private static void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
Logger.LogDebug("NetworkAvailabilityEventArgs.IsAvailable: " + e.IsAvailable.ToString(CultureInfo.InvariantCulture));
Helper.WndProcCounter++;
ScheduleReopenSocketsDueToNetworkChanges(!e.IsAvailable);
}
private static void ScheduleReopenSocketsDueToNetworkChanges(bool closeSockets = true)
{
if (closeSockets)
{
// Slept/hibernated machine may still have the sockets' status as Connected:( (unchanged) so it would not re-connect after a timeout when waking up.
// Closing the sockets when it is going to sleep/hibernate will trigger the reconnection faster when it wakes up.
Common.DoSomethingInUIThread(
() =>
{
SocketStuff s = Common.Sk;
Common.Sk = null;
s?.Close(false);
},
true);
}
if (!Common.IsMyDesktopActive())
{
PleaseReopenSocket = 0;
}
else if (PleaseReopenSocket != 10)
{
PleaseReopenSocket = 10;
}
}
}

View File

@@ -247,22 +247,24 @@ internal static class Logger
internal static void DumpStaticTypes(StringBuilder sb, int level)
{
sb.AppendLine($"[{nameof(DragDrop)}]\r\n===============");
Logger.DumpType(sb, typeof(DragDrop), 0, level);
sb.AppendLine($"[{nameof(Event)}]\r\n===============");
Logger.DumpType(sb, typeof(Event), 0, level);
sb.AppendLine($"[{nameof(Helper)}]\r\n===============");
Logger.DumpType(sb, typeof(Helper), 0, level);
sb.AppendLine($"[{nameof(Launch)}]\r\n===============");
Logger.DumpType(sb, typeof(Launch), 0, level);
sb.AppendLine($"[{nameof(Logger)}]\r\n===============");
Logger.DumpType(sb, typeof(Logger), 0, level);
sb.AppendLine($"[{nameof(MachineStuff)}]\r\n===============");
Logger.DumpType(sb, typeof(MachineStuff), 0, level);
sb.AppendLine($"[{nameof(Receiver)}]\r\n===============");
Logger.DumpType(sb, typeof(Receiver), 0, level);
sb.AppendLine($"[{nameof(Service)}]\r\n===============");
Logger.DumpType(sb, typeof(Service), 0, level);
var staticTypes = new List<Type>
{
typeof(Clipboard),
typeof(DragDrop),
typeof(Event),
typeof(InitAndCleanup),
typeof(Helper),
typeof(Launch),
typeof(Logger),
typeof(MachineStuff),
typeof(Receiver),
typeof(Service),
};
foreach (var staticType in staticTypes)
{
sb.AppendLine(CultureInfo.InvariantCulture, $"[{staticType.Name}]\r\n===============");
Logger.DumpType(sb, staticType, 0, level);
}
}
internal static bool PrivateDump(StringBuilder sb, object obj, string objName, int level, int maxLevel, bool stop)

View File

@@ -992,7 +992,7 @@ internal static class MachineStuff
Setting.Values.MatrixOneRow = !((package.Type & PackageType.MatrixTwoRowFlag) == PackageType.MatrixTwoRowFlag);
MachineMatrix = MachineMatrix; // Save
Common.ReopenSocketDueToReadError = true;
InitAndCleanup.ReopenSocketDueToReadError = true;
UpdateClientSockets("UpdateMachineMatrix");
@@ -1044,7 +1044,7 @@ internal static class MachineStuff
Common.MoveMouseToCenter();
}
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
Common.UpdateMultipleModeIconAndMenu();
}

View File

@@ -157,7 +157,7 @@ internal static class Receiver
if (!p.IsEmpty)
{
Common.HasSwitchedMachineSinceLastCopy = true;
Clipboard.HasSwitchedMachineSinceLastCopy = true;
Logger.LogDebug(string.Format(
CultureInfo.CurrentCulture,
@@ -274,7 +274,7 @@ internal static class Receiver
Common.PackageReceived.Clipboard++;
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop)
{
Common.clipboardCopiedTime = Common.GetTick();
Clipboard.clipboardCopiedTime = Common.GetTick();
GetNameOfMachineWithClipboardData(package);
SignalBigClipboardData();
}
@@ -282,10 +282,10 @@ internal static class Receiver
break;
case PackageType.MachineSwitched:
if (Common.GetTick() - Common.clipboardCopiedTime < Common.BIG_CLIPBOARD_DATA_TIMEOUT && (package.Des == Common.MachineID))
if (Common.GetTick() - Clipboard.clipboardCopiedTime < Clipboard.BIG_CLIPBOARD_DATA_TIMEOUT && (package.Des == Common.MachineID))
{
Common.clipboardCopiedTime = 0;
Common.GetRemoteClipboard("PackageType.MachineSwitched");
Clipboard.clipboardCopiedTime = 0;
Clipboard.GetRemoteClipboard("PackageType.MachineSwitched");
}
break;
@@ -297,7 +297,7 @@ internal static class Receiver
if (package.Des == Common.MachineID || package.Des == ID.ALL)
{
GetNameOfMachineWithClipboardData(package);
Common.GetRemoteClipboard("mspaint," + Common.LastMachineWithClipboardData);
Clipboard.GetRemoteClipboard("mspaint," + Clipboard.LastMachineWithClipboardData);
}
}
@@ -326,10 +326,10 @@ internal static class Receiver
Thread.UpdateThreads(thread);
string remoteMachine = package.MachineName;
System.Net.Sockets.TcpClient client = Common.ConnectToRemoteClipboardSocket(remoteMachine);
System.Net.Sockets.TcpClient client = Clipboard.ConnectToRemoteClipboardSocket(remoteMachine);
bool clientPushData = true;
if (Common.ShakeHand(ref remoteMachine, client.Client, out Stream enStream, out Stream deStream, ref clientPushData, ref package.PostAction))
if (Clipboard.ShakeHand(ref remoteMachine, client.Client, out Stream enStream, out Stream deStream, ref clientPushData, ref package.PostAction))
{
SocketStuff.SendClipboardData(client.Client, enStream);
}
@@ -360,7 +360,7 @@ internal static class Receiver
case PackageType.ClipboardText:
case PackageType.ClipboardImage:
Common.clipboardCopiedTime = 0;
Clipboard.clipboardCopiedTime = 0;
if (package.Type == PackageType.ClipboardImage)
{
Common.PackageReceived.ClipboardImage++;
@@ -372,7 +372,7 @@ internal static class Receiver
if (tcp != null)
{
Common.ReceiveClipboardDataUsingTCP(
Clipboard.ReceiveClipboardDataUsingTCP(
package,
package.Type == PackageType.ClipboardImage,
tcp);
@@ -381,10 +381,10 @@ internal static class Receiver
break;
case PackageType.HideMouse:
Common.HasSwitchedMachineSinceLastCopy = true;
Clipboard.HasSwitchedMachineSinceLastCopy = true;
Common.HideMouseCursor(true);
Helper.MainFormDotEx(false);
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
break;
default:
@@ -405,11 +405,11 @@ internal static class Receiver
internal static void GetNameOfMachineWithClipboardData(DATA package)
{
Common.LastIDWithClipboardData = package.Src;
List<MachineInf> matchingMachines = MachineStuff.MachinePool.TryFindMachineByID(Common.LastIDWithClipboardData);
Clipboard.LastIDWithClipboardData = package.Src;
List<MachineInf> matchingMachines = MachineStuff.MachinePool.TryFindMachineByID(Clipboard.LastIDWithClipboardData);
if (matchingMachines.Count >= 1)
{
Common.LastMachineWithClipboardData = matchingMachines[0].Name.Trim();
Clipboard.LastMachineWithClipboardData = matchingMachines[0].Name.Trim();
}
/*

View File

@@ -84,7 +84,7 @@ namespace MouseWithoutBorders
if ((connectedClientSocket = Common.GetConnectedClientSocket()) != null)
{
ShowStatus($"Connected from local IP Address: {connectedClientSocket.Address}.");
Common.UpdateMachineTimeAndID();
InitAndCleanup.UpdateMachineTimeAndID();
Common.MMSleep(1);
connected = true;

View File

@@ -22,6 +22,8 @@ using Microsoft.PowerToys.Telemetry;
// </history>
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
using Clipboard = MouseWithoutBorders.Core.Clipboard;
using Timer = System.Windows.Forms.Timer;
[module: SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions", Scope = "member", Target = "MouseWithoutBorders.frmMatrix.#buttonOK_Click(System.Object,System.EventArgs)", Justification = "Dotnet port with style preservation")]
@@ -110,7 +112,7 @@ namespace MouseWithoutBorders
{
SocketStuff.InvalidKeyFound = false;
showInvalidKeyMessage = false;
Common.ReopenSocketDueToReadError = true;
InitAndCleanup.ReopenSocketDueToReadError = true;
Common.ReopenSockets(true);
for (int i = 0; i < 10; i++)
@@ -780,7 +782,7 @@ namespace MouseWithoutBorders
ShowUpdateMessage();
Common.HasSwitchedMachineSinceLastCopy = true;
Clipboard.HasSwitchedMachineSinceLastCopy = true;
}
private void CheckBoxDisableCAD_CheckedChanged(object sender, EventArgs e)

View File

@@ -139,13 +139,13 @@ namespace MouseWithoutBorders
{
if (cleanup)
{
Common.Cleanup();
InitAndCleanup.Cleanup();
}
Helper.WndProcCounter++;
if (!Common.RunOnScrSaverDesktop)
{
Common.ReleaseAllKeys();
InitAndCleanup.ReleaseAllKeys();
}
Helper.RunDDHelper(true);
@@ -412,7 +412,7 @@ namespace MouseWithoutBorders
count = 0;
Common.InitDone = true;
InitAndCleanup.InitDone = true;
#if SHOW_ON_WINLOGON
if (Common.RunOnLogonDesktop)
{
@@ -423,39 +423,39 @@ namespace MouseWithoutBorders
if ((count % 2) == 0)
{
if (Common.PleaseReopenSocket == 10 || (Common.PleaseReopenSocket > 0 && count > 0 && count % 300 == 0))
if (InitAndCleanup.PleaseReopenSocket == 10 || (InitAndCleanup.PleaseReopenSocket > 0 && count > 0 && count % 300 == 0))
{
if (!Common.AtLeastOneSocketEstablished() || Common.PleaseReopenSocket == 10)
if (!Common.AtLeastOneSocketEstablished() || InitAndCleanup.PleaseReopenSocket == 10)
{
Thread.Sleep(1000);
if (Common.PleaseReopenSocket > 0)
if (InitAndCleanup.PleaseReopenSocket > 0)
{
Common.PleaseReopenSocket--;
InitAndCleanup.PleaseReopenSocket--;
}
// Double check.
if (!Common.AtLeastOneSocketEstablished())
{
Common.GetMachineName();
Logger.LogDebug("Common.pleaseReopenSocket: " + Common.PleaseReopenSocket.ToString(CultureInfo.InvariantCulture));
Logger.LogDebug("Common.pleaseReopenSocket: " + InitAndCleanup.PleaseReopenSocket.ToString(CultureInfo.InvariantCulture));
Common.ReopenSockets(false);
MachineStuff.NewDesMachineID = Common.DesMachineID = Common.MachineID;
}
}
else
{
Common.PleaseReopenSocket = 0;
InitAndCleanup.PleaseReopenSocket = 0;
}
}
if (Common.PleaseReopenSocket == Common.REOPEN_WHEN_HOTKEY)
if (InitAndCleanup.PleaseReopenSocket == InitAndCleanup.REOPEN_WHEN_HOTKEY)
{
Common.PleaseReopenSocket = 0;
InitAndCleanup.PleaseReopenSocket = 0;
Common.ReopenSockets(true);
}
else if (Common.PleaseReopenSocket == Common.REOPEN_WHEN_WSAECONNRESET)
else if (InitAndCleanup.PleaseReopenSocket == InitAndCleanup.REOPEN_WHEN_WSAECONNRESET)
{
Common.PleaseReopenSocket = 0;
InitAndCleanup.PleaseReopenSocket = 0;
Thread.Sleep(1000);
MachineStuff.UpdateClientSockets("REOPEN_WHEN_WSAECONNRESET");
}

View File

@@ -4,28 +4,6 @@
[Other Logs]
===============
= MouseWithoutBorders.Common
Comma = System.Char[]
--System.Char[] = System.Char[]: N/A
Star = System.Char[]
--System.Char[] = System.Char[]: N/A
NullSeparator = System.Char[]
--System.Char[] = System.Char[]: N/A
lastClipboardEventTime = 0
clipboardCopiedTime = 0
<LastIDWithClipboardData>k__BackingField = NONE
<NextClipboardViewer>k__BackingField = 0
<IsClipboardDataImage>k__BackingField = False
lastClipboardObject =
<HasSwitchedMachineSinceLastCopy>k__BackingField = False
ClipboardThreadOldLock = Lock
--_owningThreadId = 0
--_state = 0
--_recursionCount = 0
--_spinCount = 22
--_waiterStartTimeMs = 0
--s_contentionCount = 0
--s_maxSpinCount = 22
--s_minSpinCountForAdaptiveSpin = -100
screenWidth = 0
screenHeight = 0
lastX = 0
@@ -99,17 +77,6 @@ LegalKeyDictionary = Concurrent.ConcurrentDictionary`2[System.String,System.Byte
--_budget = ????????????
--_growLockArray = True
--_comparerIsDefaultForClasses = False
initDone = False
REOPEN_WHEN_WSAECONNRESET = -10054
REOPEN_WHEN_HOTKEY = -10055
PleaseReopenSocket = 0
ReopenSocketDueToReadError = False
<LastResumeSuspendTime>k__BackingField = ????????????
--_dateData = ????????????
--MinValue = 01/01/0001 00:00:00
--MaxValue = 31/12/9999 23:59:59
--UnixEpoch = 01/01/1970 00:00:00
lastReleaseAllKeysCall = 0
PackageSent = MouseWithoutBorders.PackageMonitor
--Keyboard = 0
--Mouse = 0
@@ -153,12 +120,6 @@ p = {X=0,Y=0}
--y = 0
--Empty = {X=0,Y=0}
<IpcChannelCreated>k__BackingField = False
BIG_CLIPBOARD_DATA_TIMEOUT = 30000
MAX_CLIPBOARD_DATA_SIZE_CAN_BE_SENT_INSTANTLY_TCP = 1048576
MAX_CLIPBOARD_FILE_SIZE_CAN_BE_SENT = 104857600
TEXT_HEADER_SIZE = 12
DATA_SIZE = 48
TEXT_TYPE_SEP = {4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}
TOGGLE_ICONS_SIZE = 4
ICON_ONE = 0
ICON_ALL = 1
@@ -195,6 +156,36 @@ WM_KEYDOWN = 256
WM_KEYUP = 257
WM_SYSKEYDOWN = 260
WM_SYSKEYUP = 261
[Clipboard]
===============
Comma = System.Char[]
--System.Char[] = System.Char[]: N/A
Star = System.Char[]
--System.Char[] = System.Char[]: N/A
NullSeparator = System.Char[]
--System.Char[] = System.Char[]: N/A
lastClipboardEventTime = 0
clipboardCopiedTime = 0
<LastIDWithClipboardData>k__BackingField = NONE
<NextClipboardViewer>k__BackingField = 0
<IsClipboardDataImage>k__BackingField = False
lastClipboardObject =
<HasSwitchedMachineSinceLastCopy>k__BackingField = False
ClipboardThreadOldLock = Lock
--_owningThreadId = 0
--_state = 0
--_recursionCount = 0
--_spinCount = 22
--_waiterStartTimeMs = 0
--s_contentionCount = 0
--s_maxSpinCount = 22
--s_minSpinCountForAdaptiveSpin = -100
BIG_CLIPBOARD_DATA_TIMEOUT = 30000
MAX_CLIPBOARD_DATA_SIZE_CAN_BE_SENT_INSTANTLY_TCP = 1048576
MAX_CLIPBOARD_FILE_SIZE_CAN_BE_SENT = 104857600
TEXT_HEADER_SIZE = 12
DATA_SIZE = 48
TEXT_TYPE_SEP = {4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}
[DragDrop]
===============
isDragging = False
@@ -249,6 +240,19 @@ actualLastPos = {X=0,Y=0}
--Empty = {X=0,Y=0}
myLastX = 0
myLastY = 0
[InitAndCleanup]
===============
initDone = False
REOPEN_WHEN_WSAECONNRESET = -10054
REOPEN_WHEN_HOTKEY = -10055
PleaseReopenSocket = 0
ReopenSocketDueToReadError = False
<LastResumeSuspendTime>k__BackingField = ????????????
--_dateData = ????????????
--MinValue = 01/01/0001 00:00:00
--MaxValue = 31/12/9999 23:59:59
--UnixEpoch = 01/01/1970 00:00:00
lastReleaseAllKeysCall = 0
[Helper]
===============
signalHelperToExit = False

View File

@@ -12,6 +12,6 @@
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
</ItemGroup>
</Project>

View File

@@ -2,11 +2,14 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);

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 Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -9,6 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
{
public ExtensionObject<ICommandItem> Model => _commandItemModel;

View File

@@ -4,12 +4,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public interface IContextItemViewModel
{
}

View File

@@ -2,11 +2,13 @@
// 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 Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
{
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Immutable;
using System.Collections.Specialized;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CommandPalette.Extensions;
@@ -29,9 +30,13 @@ public partial class MainListPage : DynamicListPage,
private bool _includeApps;
private bool _filteredItemsIncludesApps;
private InterlockedBoolean _refreshRunning;
private InterlockedBoolean _refreshRequested;
public MainListPage(IServiceProvider serviceProvider)
{
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
_serviceProvider = serviceProvider;
_tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;
@@ -55,6 +60,7 @@ public partial class MainListPage : DynamicListPage,
var settings = _serviceProvider.GetService<SettingsModel>()!;
settings.SettingsChanged += SettingsChangedHandler;
HotReloadSettings(settings);
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
IsLoading = true;
}
@@ -82,18 +88,47 @@ public partial class MainListPage : DynamicListPage,
private void ReapplySearchInBackground()
{
_ = Task.Run(() =>
_refreshRequested.Set();
if (!_refreshRunning.Set())
{
try
return;
}
_ = Task.Run(RunRefreshLoop);
}
private void RunRefreshLoop()
{
try
{
do
{
_refreshRequested.Clear();
lock (_tlcManager.TopLevelCommands)
{
if (_filteredItemsIncludesApps == _includeApps)
{
break;
}
}
var currentSearchText = SearchText;
UpdateSearchText(currentSearchText, currentSearchText);
}
catch (Exception e)
while (_refreshRequested.Value);
}
catch (Exception e)
{
Logger.LogError("Failed to reload search", e);
}
finally
{
_refreshRunning.Clear();
if (_refreshRequested.Value && _refreshRunning.Set())
{
Logger.LogError("Failed to reload search", e);
_ = Task.Run(RunRefreshLoop);
}
});
}
}
public override IListItem[] GetItems()
@@ -125,6 +160,15 @@ public partial class MainListPage : DynamicListPage,
var aliases = _serviceProvider.GetService<AliasManager>()!;
if (aliases.CheckAlias(newSearch))
{
if (_filteredItemsIncludesApps != _includeApps)
{
lock (_tlcManager.TopLevelCommands)
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
}
}
return;
}
}
@@ -137,6 +181,7 @@ public partial class MainListPage : DynamicListPage,
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
if (string.IsNullOrEmpty(newSearch))
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
RaiseItemsChanged(commands.Count);
return;

View File

@@ -98,35 +98,107 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
{
if (action is AdaptiveOpenUrlAction openUrlAction)
{
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
return;
}
// 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 (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
if (actionJson.TryGetValue("type", out var actionTypeValue))
{
// Get the data and inputs
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
var inputString = inputs.Stringify();
var actionTypeString = actionTypeValue.GetString();
Logger.LogTrace($"atString={actionTypeString}");
_ = Task.Run(() =>
var actionType = actionTypeString switch
{
try
{
var model = _formModel.Unsafe!;
if (model != null)
"Action.Submit" => ActionType.Submit,
"Action.Execute" => ActionType.Execute,
"Action.OpenUrl" => ActionType.OpenUrl,
_ => ActionType.Unsupported,
};
Logger.LogDebug($"{actionTypeString}->{actionType}");
switch (actionType)
{
case ActionType.OpenUrl:
{
var result = model.SubmitForm(inputString, dataString);
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
HandleOpenUrlAction(action, actionJson);
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
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;
}
var inputString = inputs.Stringify();
_ = Task.Run(() =>
{
try
{
var model = _formModel.Unsafe!;
if (model != null)
{
var result = model.SubmitForm(inputString, dataString);
Logger.LogDebug($"SubmitForm() returned {result}");
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
}
private static readonly string ErrorCardJson = """

View File

@@ -321,6 +321,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Search for apps, files and commands....
/// </summary>
public static string builtin_main_list_page_searchbar_placeholder {
get {
return ResourceManager.GetString("builtin_main_list_page_searchbar_placeholder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Creates a project for a new Command Palette extension.
/// </summary>

View File

@@ -227,4 +227,7 @@
<data name="builtin_disabled_extension" xml:space="preserve">
<value>Disabled</value>
</data>
<data name="builtin_main_list_page_searchbar_placeholder" xml:space="preserve">
<value>Search for apps, files and commands...</value>
</data>
</root>

View File

@@ -64,6 +64,9 @@ public partial class App : Application
this.InitializeComponent();
// Ensure types used in XAML are preserved for AOT compilation
TypePreservation.PreserveTypes();
NativeEventWaiter.WaitForEventLoop(
"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () =>
{

View File

@@ -59,8 +59,24 @@
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="MinWidth" Value="20" />
</Style.Setters>
</Style>
<Style x:Key="HotkeyTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="10" />
<Setter Property="CharacterSpacing" Value="4" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="HotkeyFontIconStyle" TargetType="FontIcon">
<Setter Property="FontSize" Value="10" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</UserControl.Resources>
@@ -155,12 +171,7 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}" />
<Border Style="{StaticResource HotkeyStyle}">
<FontIcon
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Glyph="&#xE751;" />
<FontIcon Glyph="&#xE751;" Style="{StaticResource HotkeyFontIconStyle}" />
</Border>
</StackPanel>
</Button>
@@ -179,19 +190,10 @@
Text="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" />
<StackPanel Orientation="Horizontal" Spacing="4">
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
<TextBlock
CharacterSpacing="4"
FontSize="10"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="Ctrl" />
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
</Border>
<Border Style="{StaticResource HotkeyStyle}">
<FontIcon
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Glyph="&#xE751;" />
<FontIcon Glyph="&#xE751;" Style="{StaticResource HotkeyFontIconStyle}" />
</Border>
</StackPanel>
</StackPanel>
@@ -199,7 +201,7 @@
<Button
x:Name="MoreCommandsButton"
x:Uid="MoreCommandsButton"
Padding="4"
Padding="6,4,4,4"
Click="MoreCommandsButton_Clicked"
Style="{StaticResource SubtleButtonStyle}"
ToolTipService.ToolTip="Ctrl+K"
@@ -209,32 +211,12 @@
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="More" />
<StackPanel Orientation="Horizontal" Spacing="2">
<Border
Padding="4,2,4,2"
VerticalAlignment="Center"
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="4">
<TextBlock
CharacterSpacing="4"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Ctrl" />
<StackPanel Orientation="Horizontal" Spacing="4">
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
</Border>
<Border
Padding="4,2,4,2"
VerticalAlignment="Center"
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="4">
<TextBlock
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="K" />
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="K" />
</Border>
</StackPanel>
</StackPanel>

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 Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -10,6 +11,7 @@ using Microsoft.UI.Xaml.Data;
namespace Microsoft.CmdPal.UI;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
{
public DataTemplate? Default { get; set; }

View File

@@ -29,7 +29,7 @@ public static class WindowExtensions
}
catch (NotImplementedException)
{
// SetShownInSwitchers failed. This can happen if the Explorer is not running.
// Setting IsShownInSwitchers failed. This can happen if the Explorer is not running.
}
}
}

View File

@@ -176,7 +176,11 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateAcrylic()
{
_acrylicController?.RemoveAllSystemBackdropTargets();
if (_acrylicController != null)
{
_acrylicController.RemoveAllSystemBackdropTargets();
_acrylicController.Dispose();
}
_acrylicController = GetAcrylicConfig(Content);

View File

@@ -109,10 +109,6 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />

View File

@@ -0,0 +1,40 @@
// 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.Diagnostics.CodeAnalysis;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
/// <summary>
/// This class ensures types used in XAML are preserved during AOT compilation.
/// Framework types cannot have attributes added directly to their definitions since they're external types.
/// Application types that require runtime type checking should also be preserved here if needed.
/// </summary>
internal static class TypePreservation
{
/// <summary>
/// This method ensures critical types are preserved for AOT compilation.
/// These types are used dynamically in XAML and would otherwise be trimmed.
/// </summary>
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIconSource))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.PathIcon))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DataTemplate))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.DataTemplateSelector))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ListViewItem))]
public static void PreserveTypes()
{
// This method exists only to hold the DynamicDependency attributes above.
// It must be called to ensure the types are not trimmed during AOT compilation.
// Note: We cannot add [DynamicallyAccessedMembers] directly to framework types
// since we don't own their source code. DynamicDependency is the correct approach
// for preserving external types that are used dynamically (e.g., in XAML).
// For application types that require runtime type checking (e.g., in template selectors),
// prefer adding [DynamicallyAccessedMembers] attributes directly on the type definitions.
// Only use DynamicDependency here for types we cannot modify directly.
}
}

View File

@@ -1,23 +0,0 @@
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<Assembly Name="Microsoft.WinUI">
<Type Name="Microsoft.UI.Xaml.Controls.FontIconSource" Dynamic="Required All" />
<Type Name="Microsoft.UI.Xaml.DataTemplate" Dynamic="Required All" />
<Type Name="Microsoft.UI.Xaml.Controls.DataTemplateSelector" Dynamic="Required All" />
<Type Name="Microsoft.UI.Xaml.Controls.ListViewItem" Dynamic="Required All" />
</Assembly>
<!-- Add ViewModel types for AOT compatibility -->
<Assembly Name="Microsoft.CmdPal.Core.ViewModels">
<Type Name="Microsoft.CmdPal.Core.ViewModels.CommandContextItemViewModel" Dynamic="Required All" />
<Type Name="Microsoft.CmdPal.Core.ViewModels.SeparatorContextItemViewModel" Dynamic="Required All" />
<Type Name="Microsoft.CmdPal.Core.ViewModels.IContextItemViewModel" Dynamic="Required All" />
<Type Name="Microsoft.CmdPal.Core.ViewModels.CommandItemViewModel" Dynamic="Required All" />
</Assembly>
<!-- Add UI types for AOT compatibility -->
<Assembly Name="Microsoft.CmdPal.UI">
<Type Name="Microsoft.CmdPal.UI.ContextItemTemplateSelector" Dynamic="Required All" />
</Assembly>
</Application>
</Directives>

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -6,12 +6,15 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class ExtendedCalculatorParserTests
public class ExtendedCalculatorParserTests : CommandPaletteUnitTestBase
{
[DataTestMethod]
[DataRow(null)]
@@ -28,7 +31,7 @@ public class ExtendedCalculatorParserTests
[DataRow("[10,10]")] // '[10,10]' is interpreted as array by mages engine
public void Interpret_NoResult_WhenCalled(string input)
{
var settings = new SettingsManager();
var settings = new Settings();
var result = CalculateEngine.Interpret(settings, input, CultureInfo.CurrentCulture, out _);
@@ -68,7 +71,7 @@ public class ExtendedCalculatorParserTests
[DynamicData(nameof(Interpret_NoErrors_WhenCalledWithRounding_Data))]
public void Interpret_NoErrors_WhenCalledWithRounding(string input, decimal expectedResult)
{
var settings = new SettingsManager();
var settings = new Settings();
// Act
// Using InvariantCulture since this is internal
@@ -90,7 +93,7 @@ public class ExtendedCalculatorParserTests
public void Interpret_GreaterPrecision_WhenCalled(string input, decimal expectedResult)
{
// Arrange
var settings = new SettingsManager();
var settings = new Settings();
// Act
// Using InvariantCulture since this is internal
@@ -114,7 +117,7 @@ public class ExtendedCalculatorParserTests
{
// Arrange
var cultureInfo = CultureInfo.GetCultureInfo(cultureName);
var settings = new SettingsManager();
var settings = new Settings();
// Act
var result = CalculateEngine.Interpret(settings, input, cultureInfo, out _);
@@ -175,7 +178,7 @@ public class ExtendedCalculatorParserTests
public void Interpret_MustReturnResult_WhenResultIsZero(string input)
{
// Arrange
var settings = new SettingsManager();
var settings = new Settings();
// Act
// Using InvariantCulture since this is internal
@@ -203,7 +206,7 @@ public class ExtendedCalculatorParserTests
public void Interpret_MustReturnExpectedResult_WhenCalled(string input, decimal expectedResult)
{
// Arrange
var settings = new SettingsManager();
var settings = new Settings();
// Act
// Using en-us culture to have a fixed number style
@@ -226,7 +229,7 @@ public class ExtendedCalculatorParserTests
{
// Arrange
var translator = NumberTranslator.Create(new CultureInfo(sourceCultureName, false), new CultureInfo("en-US", false));
var settings = new SettingsManager();
var settings = new Settings();
// Act
// Using en-us culture to have a fixed number style

View File

@@ -14,5 +14,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Pages;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[DataTestMethod]
[DataRow("2+2", "4")]
[DataRow("5*3", "15")]
[DataRow("10/2", "5")]
[DataRow("sqrt(16)", "4")]
[DataRow("2^3", "8")]
public void TopLevelPageQueryTest(string input, string expectedResult)
{
var settings = new Settings();
var page = new CalculatorListPage(settings);
// Simulate query execution
page.UpdateSearchText(string.Empty, input);
var result = page.GetItems();
Assert.IsTrue(result.Length == 1, "Valid input should always return result");
var firstResult = result.FirstOrDefault();
Assert.IsNotNull(result);
Assert.IsTrue(
firstResult.Title.Contains(expectedResult),
$"Expected result to contain '{expectedResult}' but got '{firstResult.Title}'");
}
[TestMethod]
public void EmptyQueryTest()
{
var settings = new Settings();
var page = new CalculatorListPage(settings);
page.UpdateSearchText("abc", string.Empty);
var results = page.GetItems();
Assert.IsNotNull(results);
var firstItem = results.FirstOrDefault();
Assert.AreEqual("Type an equation...", firstItem.Title);
}
[TestMethod]
public void InvalidExpressionTest()
{
var settings = new Settings();
var page = new CalculatorListPage(settings);
// Simulate query execution
page.UpdateSearchText(string.Empty, "invalid expression");
var result = page.GetItems().FirstOrDefault();
Assert.AreEqual("Type an equation...", result.Title);
}
[DataTestMethod]
[DataRow("sin(60)", "-0.30481", CalculateEngine.TrigMode.Radians)]
[DataRow("sin(60)", "0.866025", CalculateEngine.TrigMode.Degrees)]
[DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians)]
public void TrigModeSettingsTest(string input, string expected, CalculateEngine.TrigMode trigMode)
{
var settings = new Settings(trigUnit: trigMode);
var page = new CalculatorListPage(settings);
page.UpdateSearchText(string.Empty, input);
var result = page.GetItems().FirstOrDefault();
Assert.IsNotNull(result);
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}");
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Calc.Helper;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
public class Settings : ISettingsInterface
{
private readonly CalculateEngine.TrigMode trigUnit;
private readonly bool inputUseEnglishFormat;
private readonly bool outputUseEnglishFormat;
private readonly bool closeOnEnter;
public Settings(
CalculateEngine.TrigMode trigUnit = CalculateEngine.TrigMode.Radians,
bool inputUseEnglishFormat = false,
bool outputUseEnglishFormat = false,
bool closeOnEnter = true)
{
this.trigUnit = trigUnit;
this.inputUseEnglishFormat = inputUseEnglishFormat;
this.outputUseEnglishFormat = outputUseEnglishFormat;
this.closeOnEnter = closeOnEnter;
}
public CalculateEngine.TrigMode TrigUnit => trigUnit;
public bool InputUseEnglishFormat => inputUseEnglishFormat;
public bool OutputUseEnglishFormat => outputUseEnglishFormat;
public bool CloseOnEnter => closeOnEnter;
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class SettingsManagerTests
{
[TestMethod]
public void SettingsManagerInitializationTest()
{
// Act
var settingsManager = new SettingsManager();
// Assert
Assert.IsNotNull(settingsManager);
Assert.IsNotNull(settingsManager.Settings);
}
[TestMethod]
public void SettingsInterfaceTest()
{
// Act
ISettingsInterface settings = new SettingsManager();
// Assert
Assert.IsNotNull(settings);
Assert.IsTrue(settings.TrigUnit == CalculateEngine.TrigMode.Radians);
Assert.IsFalse(settings.InputUseEnglishFormat);
Assert.IsFalse(settings.OutputUseEnglishFormat);
Assert.IsTrue(settings.CloseOnEnter);
}
[TestMethod]
public void MockSettingsTest()
{
// Act
var settings = new Settings(
trigUnit: CalculateEngine.TrigMode.Degrees,
inputUseEnglishFormat: true,
outputUseEnglishFormat: true,
closeOnEnter: false);
// Assert
Assert.IsNotNull(settings);
Assert.AreEqual(CalculateEngine.TrigMode.Degrees, settings.TrigUnit);
Assert.IsTrue(settings.InputUseEnglishFormat);
Assert.IsTrue(settings.OutputUseEnglishFormat);
Assert.IsFalse(settings.CloseOnEnter);
}
}

View File

@@ -18,5 +18,6 @@
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.Registry.Helpers;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Registry.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[DataTestMethod]
[DataRow("HKLM", "HKEY_LOCAL_MACHINE")]
[DataRow("HKCU", "HKEY_CURRENT_USER")]
[DataRow("HKCR", "HKEY_CLASSES_ROOT")]
[DataRow("HKU", "HKEY_USERS")]
[DataRow("HKCC", "HKEY_CURRENT_CONFIG")]
public void TopLevelPageQueryTest(string input, string expectedKeyName)
{
var settings = new Settings();
var page = new RegistryListPage(settings);
var results = page.Query(input);
Assert.IsNotNull(results);
Assert.IsTrue(results.Count > 0, "No items matched the query.");
var firstItem = results.FirstOrDefault();
Assert.IsNotNull(firstItem, "No items matched the query.");
Assert.IsTrue(
firstItem.Title.Contains(expectedKeyName, System.StringComparison.OrdinalIgnoreCase),
$"Expected to match '{expectedKeyName}' but got '{firstItem.Title}'");
}
[TestMethod]
public void EmptyQueryTest()
{
var settings = new Settings();
var page = new RegistryListPage(settings);
var results = page.Query(string.Empty);
Assert.IsNotNull(results);
// Empty query should return all base keys
Assert.IsTrue(results.Count >= 5, "Expected at least 5 base registry keys.");
}
[TestMethod]
public void NullQueryTest()
{
var settings = new Settings();
var page = new RegistryListPage(settings);
var results = page.Query(null);
Assert.IsNotNull(results);
Assert.AreEqual(0, results.Count, "Null query should return empty results.");
}
[TestMethod]
public void InvalidBaseKeyTest()
{
var settings = new Settings();
var page = new RegistryListPage(settings);
var results = page.Query("INVALID_KEY");
Assert.IsNotNull(results);
Assert.AreEqual(0, results.Count, "Invalid query should return empty results.");
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Registry.Helpers;
namespace Microsoft.CmdPal.Ext.Registry.UnitTests;
public class Settings : ISettingsInterface
{
public Settings()
{
// Currently no specific settings for Registry extension
}
}

View File

@@ -142,11 +142,7 @@ public class QueryTests : CommandPaletteUnitTestBase
// UEFI Firmware Settings command should exist
Assert.IsNotNull(result);
var firstItem = result.FirstOrDefault();
Assert.IsNotNull(firstItem, "No items matched the query.");
var containsFirmwareSettings = firstItem.Title.Contains("UEFI Firmware Settings", StringComparison.OrdinalIgnoreCase);
Assert.IsTrue(
containsFirmwareSettings == hasCommand,
$"Expected to match 'UEFI Firmware Settings' but got '{firstItem.Title}'");
var firstItemIsUefiCommand = firstItem?.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase) ?? false;
Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI Firmware Settings' but got '{firstItem?.Title}'");
}
}

View File

@@ -367,7 +367,7 @@ public class AvailableResultsListTests
public void UnixTimestampSecondsFormat()
{
// Setup
string formatLabel = "Unix epoch time";
var formatLabel = "Unix epoch time";
DateTime timeValue = DateTime.Now.ToUniversalTime();
var settings = new SettingsManager();
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
@@ -384,7 +384,7 @@ public class AvailableResultsListTests
public void UnixTimestampMillisecondsFormat()
{
// Setup
string formatLabel = "Unix epoch time in milliseconds";
var formatLabel = "Unix epoch time in milliseconds";
DateTime timeValue = DateTime.Now.ToUniversalTime();
var settings = new SettingsManager();
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
@@ -401,7 +401,7 @@ public class AvailableResultsListTests
public void WindowsFileTimeFormat()
{
// Setup
string formatLabel = "Windows file time (Int64 number)";
var formatLabel = "Windows file time (Int64 number)";
DateTime timeValue = DateTime.Now;
var settings = new SettingsManager();
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
@@ -418,7 +418,7 @@ public class AvailableResultsListTests
public void ValidateEraResult()
{
// Setup
string formatLabel = "Era";
var formatLabel = "Era";
DateTime timeValue = DateTime.Now;
var settings = new SettingsManager();
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);
@@ -435,7 +435,7 @@ public class AvailableResultsListTests
public void ValidateEraAbbreviationResult()
{
// Setup
string formatLabel = "Era abbreviation";
var formatLabel = "Era abbreviation";
DateTime timeValue = DateTime.Now;
var settings = new SettingsManager();
var helperResults = AvailableResultsList.GetList(true, settings, null, null, timeValue);

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class BasicTests
{
[TestMethod]
public void BasicTest()
{
// This is a basic test to verify the test project can run
Assert.IsTrue(true);
}
[TestMethod]
public void DateTimeTest()
{
// Test basic DateTime functionality
var now = DateTime.Now;
Assert.IsNotNull(now);
Assert.IsTrue(now > DateTime.MinValue);
}
}

View File

@@ -40,7 +40,7 @@ public class FallbackTimeDateItemTests
public void FallbackQueryTests(string query, string expectedTitle)
{
// Setup
var settingsManager = new SettingsManager();
var settingsManager = new Settings();
DateTime now = new DateTime(2025, 7, 1, 12, 0, 0); // Fixed date for testing
var fallbackItem = new FallbackTimeDateItem(settingsManager, now);
@@ -66,7 +66,7 @@ public class FallbackTimeDateItemTests
public void InvalidQueryTests(string query)
{
// Setup
var settingsManager = new SettingsManager();
var settingsManager = new Settings();
DateTime now = new DateTime(2025, 7, 1, 12, 0, 0); // Fixed date for testing
var fallbackItem = new FallbackTimeDateItem(settingsManager, now);
@@ -83,4 +83,26 @@ public class FallbackTimeDateItemTests
Assert.Fail($"UpdateQuery should not throw exceptions: {ex.Message}");
}
}
[DataTestMethod]
public void DisableFallbackItemTest()
{
// Setup
var settingsManager = new Settings(enableFallbackItems: false);
DateTime now = new DateTime(2025, 7, 1, 12, 0, 0); // Fixed date for testing
var fallbackItem = new FallbackTimeDateItem(settingsManager, now);
// Act & Assert - Test that UpdateQuery doesn't throw exceptions
try
{
fallbackItem.UpdateQuery("now");
Assert.AreEqual(string.Empty, fallbackItem.Title, "Title should be empty when disable fallback item");
Assert.AreEqual(string.Empty, fallbackItem.Subtitle, "Subtitle should be empty when disable fallback item");
}
catch (Exception ex)
{
Assert.Fail($"UpdateQuery should not throw exceptions: {ex.Message}");
}
}
}

View File

@@ -1,127 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class IconTests
{
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
{
// Set culture to 'en-us'
originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
}
[TestCleanup]
public void CleanUp()
{
// Set culture to original value
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
[TestMethod]
public void TimeDateCommandsProvider_HasIcon()
{
// Setup
var provider = new TimeDateCommandsProvider();
// Act
var icon = provider.Icon;
// Assert
Assert.IsNotNull(icon, "Provider should have an icon");
}
[TestMethod]
public void TimeDateCommandsProvider_TopLevelCommands_HaveIcons()
{
// Setup
var provider = new TimeDateCommandsProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0, "Should have at least one top-level command");
foreach (var command in commands)
{
Assert.IsNotNull(command.Icon, "Each command should have an icon");
}
}
[TestMethod]
public void AvailableResults_HaveIcons()
{
// Setup
var settings = new SettingsManager();
// Act
var results = AvailableResultsList.GetList(true, settings);
// Assert
Assert.IsNotNull(results);
Assert.IsTrue(results.Count > 0, "Should have results");
foreach (var result in results)
{
Assert.IsNotNull(result.GetIconInfo(), $"Result '{result.Label}' should have an icon");
}
}
[DataTestMethod]
[DataRow(ResultIconType.Time, "\uE823")]
[DataRow(ResultIconType.Date, "\uE787")]
[DataRow(ResultIconType.DateTime, "\uEC92")]
public void ResultHelper_CreateListItem_PreservesIcon(ResultIconType resultIconType, string expectedIcon)
{
// Setup
var availableResult = new AvailableResult
{
Label = "Test Label",
Value = "Test Value",
IconType = resultIconType,
};
// Act
var listItem = availableResult.ToListItem();
var icon = listItem.Icon;
// Assert
Assert.IsNotNull(listItem);
Assert.IsNotNull(listItem.Icon, "ListItem should preserve the icon from AvailableResult");
Assert.AreEqual(expectedIcon, icon.Dark.Icon, $"Icon for {resultIconType} should match expected value");
}
[TestMethod]
public void Icons_AreNotEmpty()
{
// Setup
var settings = new SettingsManager();
var results = AvailableResultsList.GetList(true, settings);
// Act & Assert
foreach (var result in results)
{
Assert.IsNotNull(result.GetIconInfo(), $"Result '{result.Label}' should have an icon");
Assert.IsFalse(string.IsNullOrWhiteSpace(result.GetIconInfo().ToString()), $"Icon for '{result.Label}' should not be empty");
}
}
}

View File

@@ -19,5 +19,6 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,13 +6,14 @@ using System;
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.CmdPal.Ext.TimeDate.Pages;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class QueryTests
public class QueryTests : CommandPaletteUnitTestBase
{
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
@@ -46,7 +47,7 @@ public class QueryTests
public void CountBasicQueries(string query, int expectedMinResultCount)
{
// Setup
var settings = new SettingsManager();
var settings = new Settings();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
@@ -58,30 +59,32 @@ public class QueryTests
}
[DataTestMethod]
[DataRow("time")]
[DataRow("date")]
[DataRow("year")]
[DataRow("now")]
[DataRow("current")]
[DataRow("")]
[DataRow("now::10:10:10")] // Windows file time
public void AllQueriesReturnResults(string query)
[DataRow("time", "time")]
[DataRow("date", "date")]
[DataRow("year", "year")]
[DataRow("now", "now")]
[DataRow("year", "year")]
public void BasicQueryTest(string input, string expectedMatchTerm)
{
// Setup
var settings = new SettingsManager();
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, input);
var resultLists = page.GetItems();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
var result = Query(input, resultLists);
// Assert
Assert.IsNotNull(results);
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return at least one result");
Assert.IsNotNull(result);
Assert.IsTrue(result.Length > 0, "No items matched the query.");
var firstItem = result.FirstOrDefault();
Assert.IsNotNull(firstItem, "No items matched the query.");
Assert.IsTrue(
firstItem.Title.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase) ||
firstItem.Subtitle.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase),
$"Expected to match '{expectedMatchTerm}' in title or subtitle but got '{firstItem.Title}' - '{firstItem.Subtitle}'");
}
[DataTestMethod]
[DataRow("time", "Time")]
[DataRow("date", "Date")]
[DataRow("now", "Now")]
[DataRow("unix", "Unix epoch time")]
[DataRow("unix epoch time in milli", "Unix epoch time in milliseconds")]
[DataRow("file", "Windows file time (Int64 number)")]
@@ -98,12 +101,8 @@ public class QueryTests
[DataRow("month", "Month")]
[DataRow("month of year", "Month of the year")]
[DataRow("month and d", "Month and day")]
[DataRow("month and y", "Month and year")]
[DataRow("year", "Year")]
[DataRow("era", "Era")]
[DataRow("era a", "Era abbreviation")]
[DataRow("universal", "Universal time format: YYYY-MM-DD hh:mm:ss")]
[DataRow("iso", "ISO 8601")]
[DataRow("rfc", "RFC1123")]
[DataRow("time::12:30", "Time")]
[DataRow("date::10.10.2022", "Date")]
@@ -114,40 +113,19 @@ public class QueryTests
[DataRow("week num", "Week of the year (Calendar week, Week number)")]
[DataRow("days in mo", "Days in month")]
[DataRow("Leap y", "Leap year")]
public void CanFindFormatResult(string query, string expectedSubtitle)
public void FormatDateQueryTest(string input, string expectedMatchTerm)
{
// Setup
var settings = new SettingsManager();
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, input);
var resultLists = page.GetItems();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
// Assert
var matchingResult = results.FirstOrDefault(x => x.Subtitle?.StartsWith(expectedSubtitle, StringComparison.CurrentCulture) == true);
Assert.IsNotNull(matchingResult, $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
}
[DataTestMethod]
[DataRow("12:30", "Time")]
[DataRow("10.10.2022", "Date")]
[DataRow("u1646408119", "Date and time")]
[DataRow("u+1646408119", "Date and time")]
[DataRow("u-1646408119", "Date and time")]
[DataRow("ums1646408119", "Date and time")]
[DataRow("ums+1646408119", "Date and time")]
[DataRow("ums-1646408119", "Date and time")]
[DataRow("ft637820085517321977", "Date and time")]
public void DateTimeNumberOnlyInput(string query, string expectedSubtitle)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
// Assert
var matchingResult = results.FirstOrDefault(x => x.Subtitle?.StartsWith(expectedSubtitle, StringComparison.CurrentCulture) == true);
Assert.IsNotNull(matchingResult, $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
var firstItem = resultLists.FirstOrDefault();
Assert.IsNotNull(firstItem, "No items matched the query.");
Assert.IsTrue(
firstItem.Title.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase) ||
firstItem.Subtitle.Contains(expectedMatchTerm, System.StringComparison.OrdinalIgnoreCase),
$"Expected to match '{expectedMatchTerm}' in title or subtitle but got '{firstItem.Title}' - '{firstItem.Subtitle}'");
}
[DataTestMethod]
@@ -157,24 +135,6 @@ public class QueryTests
[DataRow("time:eeee")]
[DataRow("time::eeee")]
[DataRow("time//eeee")]
public void InvalidInputShowsErrorResults(string query)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
// Assert
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return at least one result");
// For invalid input, cmdpal returns an error result
var hasErrorResult = results.Any(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
Assert.IsTrue(hasErrorResult, $"Query '{query}' should return an error result for invalid input");
}
[DataTestMethod]
[DataRow("ug1646408119")] // Invalid prefix
[DataRow("u9999999999999")] // Unix number + prefix is longer than 12 characters
[DataRow("ums999999999999999")] // Unix number in milliseconds + prefix is longer than 17 characters
@@ -194,116 +154,33 @@ public class QueryTests
[DataRow("10.aa.22")]
[DataRow("12::55")]
[DataRow("12:aa:55")]
public void InvalidNumberInputShowsErrorMessage(string query)
public void InvalidInputShowsErrorResults(string query)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, query);
var results = page.GetItems();
// Assert
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
Assert.IsTrue(results.Count > 0, $"Should return at least one result (error message) for invalid query '{query}'");
Assert.IsTrue(results.Length > 0, $"Query '{query}' should return at least one result");
// Check if we get an error result
var errorResult = results.FirstOrDefault(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
Assert.IsNotNull(errorResult, $"Should return an error result for invalid query '{query}'");
var firstItem = results.FirstOrDefault();
Assert.IsTrue(firstItem.Title.StartsWith("Error: Invalid input", StringComparison.CurrentCulture), $"Query '{query}' should return an error result for invalid input");
}
[DataTestMethod]
[DataRow("10.10aa")] // Input contains <Number>.<Number> (Can be part of a date.)
[DataRow("10:10aa")] // Input contains <Number>:<Number> (Can be part of a time.)
[DataRow("10/10aa")] // Input contains <Number>/<Number> (Can be part of a date.)
public void InvalidInputNotShowsErrorMessage(string query)
[DataRow("")]
[DataRow(null)]
public void EmptyQueryReturnsAllResults(string input)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText("abc", input);
var results = page.GetItems();
// Assert
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
// These queries are ambiguous and cmdpal returns an error for them
// This test might need to be adjusted based on actual cmdpal behavior
if (results.Count > 0)
{
var hasErrorResult = results.Any(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
// For these ambiguous inputs, cmdpal may return error results, which is acceptable
// We just verify that the system handles them gracefully (doesn't crash)
Assert.IsTrue(true, $"Query '{query}' handled gracefully");
}
}
[DataTestMethod]
[DataRow("time", "time", true)] // Full word match should work
[DataRow("date", "date", true)] // Full word match should work
[DataRow("now", "now", true)] // Full word match should work
[DataRow("year", "year", true)] // Full word match should work
[DataRow("abcdefg", "", false)] // Invalid query should return error
public void ValidateBehaviorOnSearchQueries(string query, string expectedMatchTerm, bool shouldHaveValidResults)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
// Assert
Assert.IsNotNull(results, $"Results should not be null for query '{query}'");
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return at least one result");
if (shouldHaveValidResults)
{
// Should have non-error results
var hasValidResult = results.Any(r => !r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
Assert.IsTrue(hasValidResult, $"Query '{query}' should return valid (non-error) results");
if (!string.IsNullOrEmpty(expectedMatchTerm))
{
var hasMatchingResult = results.Any(r =>
r.Title?.Contains(expectedMatchTerm, StringComparison.CurrentCultureIgnoreCase) == true ||
r.Subtitle?.Contains(expectedMatchTerm, StringComparison.CurrentCultureIgnoreCase) == true);
Assert.IsTrue(hasMatchingResult, $"Query '{query}' should return results containing '{expectedMatchTerm}'");
}
}
else
{
// Should have error results
var hasErrorResult = results.Any(r => r.Title?.StartsWith("Error: Invalid input", StringComparison.CurrentCulture) == true);
Assert.IsTrue(hasErrorResult, $"Query '{query}' should return error results for invalid input");
}
}
[TestMethod]
public void EmptyQueryReturnsAllResults()
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, string.Empty);
// Assert
Assert.IsNotNull(results);
Assert.IsTrue(results.Count > 0, "Empty query should return all available results");
}
[TestMethod]
public void NullQueryReturnsAllResults()
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, null);
// Assert
Assert.IsNotNull(results);
Assert.IsTrue(results.Count > 0, "Null query should return all available results");
Assert.IsTrue(results.Length > 0, $"Empty query should return results");
}
[DataTestMethod]
@@ -312,39 +189,34 @@ public class QueryTests
[DataRow("iso utc", "ISO 8601 UTC")]
[DataRow("iso zone", "ISO 8601 with time zone")]
[DataRow("iso utc zone", "ISO 8601 UTC with time zone")]
public void UTCRelatedQueries(string query, string expectedSubtitle)
public void TimeZoneQuery(string query, string expectedSubtitle)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, query);
var resultsList = page.GetItems();
var results = Query(query, resultsList);
// Assert
Assert.IsNotNull(results);
Assert.IsTrue(results.Count > 0, $"Query '{query}' should return results");
var matchingResult = results.FirstOrDefault(x => x.Subtitle?.StartsWith(expectedSubtitle, StringComparison.CurrentCulture) == true);
Assert.IsNotNull(matchingResult, $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
var firstResult = results.FirstOrDefault();
Assert.IsTrue(firstResult.Subtitle.StartsWith(expectedSubtitle, StringComparison.CurrentCulture), $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
}
[DataTestMethod]
[DataRow("time::12:30:45")]
[DataRow("date::2023-12-25")]
[DataRow("now::u1646408119")]
[DataRow("current::ft637820085517321977")]
public void DelimiterQueriesReturnResults(string query)
[DataRow("time::12:30:45", "12:30 PM")]
[DataRow("date::2023-12-25", "12/25/2023")]
[DataRow("now::u1646408119", "132908817190000000")]
public void DelimiterQueriesReturnResults(string query, string expectedResult)
{
// Setup
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, query);
var settings = new Settings();
var page = new TimeDateExtensionPage(settings);
page.UpdateSearchText(string.Empty, query);
var resultsList = page.GetItems();
// Assert
Assert.IsNotNull(results);
// Delimiter queries should return results even if parsing fails (error results)
Assert.IsTrue(results.Count > 0, $"Delimiter query '{query}' should return at least one result");
Assert.IsNotNull(resultsList);
var firstResult = resultsList.FirstOrDefault();
Assert.IsTrue(firstResult.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
}
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
public class Settings : ISettingsInterface
{
private readonly int firstWeekOfYear;
private readonly int firstDayOfWeek;
private readonly bool enableFallbackItems;
private readonly bool timeWithSecond;
private readonly bool dateWithWeekday;
private readonly List<string> customFormats;
public Settings(
int firstWeekOfYear = -1,
int firstDayOfWeek = -1,
bool enableFallbackItems = true,
bool timeWithSecond = false,
bool dateWithWeekday = false,
List<string> customFormats = null)
{
this.firstWeekOfYear = firstWeekOfYear;
this.firstDayOfWeek = firstDayOfWeek;
this.enableFallbackItems = enableFallbackItems;
this.timeWithSecond = timeWithSecond;
this.dateWithWeekday = dateWithWeekday;
this.customFormats = customFormats ?? new List<string>();
}
public int FirstWeekOfYear => firstWeekOfYear;
public int FirstDayOfWeek => firstDayOfWeek;
public bool EnableFallbackItems => enableFallbackItems;
public bool TimeWithSecond => timeWithSecond;
public bool DateWithWeekday => dateWithWeekday;
public List<string> CustomFormats => customFormats;
}

View File

@@ -1,85 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class SettingsManagerTests
{
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
{
// Set culture to 'en-us'
originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
}
[TestCleanup]
public void Cleanup()
{
// Restore original culture
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
[TestMethod]
public void SettingsManagerInitializationTest()
{
// Act
var settingsManager = new SettingsManager();
// Assert
Assert.IsNotNull(settingsManager);
Assert.IsNotNull(settingsManager.Settings);
}
[TestMethod]
public void DefaultSettingsValidation()
{
// Act
var settingsManager = new SettingsManager();
// Assert - Check that properties are accessible
var enableFallback = settingsManager.EnableFallbackItems;
var timeWithSecond = settingsManager.TimeWithSecond;
var dateWithWeekday = settingsManager.DateWithWeekday;
var firstWeekOfYear = settingsManager.FirstWeekOfYear;
var firstDayOfWeek = settingsManager.FirstDayOfWeek;
var customFormats = settingsManager.CustomFormats;
Assert.IsNotNull(customFormats);
}
[TestMethod]
public void SettingsPropertiesAccessibilityTest()
{
// Setup
var settingsManager = new SettingsManager();
// Act & Assert - Verify all properties are accessible without exception
try
{
_ = settingsManager.EnableFallbackItems;
_ = settingsManager.TimeWithSecond;
_ = settingsManager.DateWithWeekday;
_ = settingsManager.FirstWeekOfYear;
_ = settingsManager.FirstDayOfWeek;
_ = settingsManager.CustomFormats;
}
catch (Exception ex)
{
Assert.Fail($"Settings properties should be accessible: {ex.Message}");
}
}
}

View File

@@ -20,5 +20,6 @@
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,60 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Reflection;
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WindowWalker.UnitTests;
[TestClass]
public class PluginSettingsTests
{
[DataTestMethod]
[DataRow("ResultsFromVisibleDesktopOnly")]
[DataRow("SubtitleShowPid")]
[DataRow("SubtitleShowDesktopName")]
[DataRow("ConfirmKillProcess")]
[DataRow("KillProcessTree")]
[DataRow("OpenAfterKillAndClose")]
[DataRow("HideKillProcessOnElevatedProcesses")]
[DataRow("HideExplorerSettingInfo")]
[DataRow("InMruOrder")]
public void DoesSettingExist(string name)
{
// Setup
Type settings = SettingsManager.Instance?.GetType();
// Act
var result = settings?.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
// Assert
Assert.IsNotNull(result);
}
[DataTestMethod]
[DataRow("ResultsFromVisibleDesktopOnly", false)]
[DataRow("SubtitleShowPid", false)]
[DataRow("SubtitleShowDesktopName", true)]
[DataRow("ConfirmKillProcess", true)]
[DataRow("KillProcessTree", false)]
[DataRow("OpenAfterKillAndClose", false)]
[DataRow("HideKillProcessOnElevatedProcesses", false)]
[DataRow("HideExplorerSettingInfo", true)]
[DataRow("InMruOrder", true)]
public void DefaultValues(string name, bool valueExpected)
{
// Setup
SettingsManager setting = SettingsManager.Instance;
// Act
PropertyInfo propertyInfo = setting?.GetType()?.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
var result = propertyInfo?.GetValue(setting);
// Assert
Assert.AreEqual(valueExpected, result);
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
namespace Microsoft.CmdPal.Ext.WindowWalker.UnitTests;
public class Settings : ISettingsInterface
{
private readonly bool resultsFromVisibleDesktopOnly;
private readonly bool subtitleShowPid;
private readonly bool subtitleShowDesktopName;
private readonly bool confirmKillProcess;
private readonly bool killProcessTree;
private readonly bool openAfterKillAndClose;
private readonly bool hideKillProcessOnElevatedProcesses;
private readonly bool hideExplorerSettingInfo;
private readonly bool inMruOrder;
public Settings(
bool resultsFromVisibleDesktopOnly = false,
bool subtitleShowPid = false,
bool subtitleShowDesktopName = true,
bool confirmKillProcess = true,
bool killProcessTree = false,
bool openAfterKillAndClose = false,
bool hideKillProcessOnElevatedProcesses = false,
bool hideExplorerSettingInfo = true,
bool inMruOrder = true)
{
this.resultsFromVisibleDesktopOnly = resultsFromVisibleDesktopOnly;
this.subtitleShowPid = subtitleShowPid;
this.subtitleShowDesktopName = subtitleShowDesktopName;
this.confirmKillProcess = confirmKillProcess;
this.killProcessTree = killProcessTree;
this.openAfterKillAndClose = openAfterKillAndClose;
this.hideKillProcessOnElevatedProcesses = hideKillProcessOnElevatedProcesses;
this.hideExplorerSettingInfo = hideExplorerSettingInfo;
this.inMruOrder = inMruOrder;
}
public bool ResultsFromVisibleDesktopOnly => resultsFromVisibleDesktopOnly;
public bool SubtitleShowPid => subtitleShowPid;
public bool SubtitleShowDesktopName => subtitleShowDesktopName;
public bool ConfirmKillProcess => confirmKillProcess;
public bool KillProcessTree => killProcessTree;
public bool OpenAfterKillAndClose => openAfterKillAndClose;
public bool HideKillProcessOnElevatedProcesses => hideKillProcessOnElevatedProcesses;
public bool HideExplorerSettingInfo => hideExplorerSettingInfo;
public bool InMruOrder => inMruOrder;
}

View File

@@ -67,9 +67,8 @@ public class BasicTests : CommandPaletteTestBase
Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal Profiles");
searchFileItem.DoubleClick();
SetSearchBox("PowerShell");
Assert.IsNotNull(this.Find<NavigationViewItem>("PowerShell"));
// SetSearchBox("PowerShell");
// Assert.IsNotNull(this.Find<NavigationViewItem>("PowerShell"));
}
[TestMethod]
@@ -95,9 +94,9 @@ public class BasicTests : CommandPaletteTestBase
Assert.AreEqual(searchFileItem.Name, "Registry");
searchFileItem.DoubleClick();
SetSearchBox("HKEY_LOCAL_MACHINE");
Assert.IsNotNull(this.Find<NavigationViewItem>("HKEY_LOCAL_MACHINE\\SECURITY"));
// Type the string will cause strange behavior.so comment it out for now.
// SetSearchBox(@"HKEY_LOCAL_MACHINE");
// Assert.IsNotNull(this.Find<NavigationViewItem>(@"HKEY_LOCAL_MACHINE\SECURITY"));
}
[TestMethod]

View File

@@ -45,4 +45,27 @@ public class CommandPaletteTestBase : UITestBase
Assert.IsNotNull(contextMenuButton, "Context menu button not found.");
contextMenuButton.Click();
}
protected void FindDefaultAppDialogAndClickButton()
{
try
{
// win11
var chooseDialog = FindByClassName("NamedContainerAutomationPeer", global: true);
chooseDialog.Find<Button>("Just once").Click();
}
catch
{
try
{
// win10
var chooseDialog = FindByClassName("Shell_Flyout", global: true);
chooseDialog.Find<Button>("OK").Click();
}
catch
{
}
}
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -18,6 +19,7 @@ public class IndexerTests : CommandPaletteTestBase
{
private const string TestFileContent = "This is Indexer UI test sample";
private const string TestFileName = "indexer_test_item.txt";
private const string TestFileBaseName = "indexer_test_item";
private const string TestFolderName = "Downloads";
public IndexerTests()
@@ -67,11 +69,14 @@ public class IndexerTests : CommandPaletteTestBase
searchItem.Click();
var openButton = this.Find<Button>("Open");
var openButton = this.Find<Button>("Open with");
Assert.IsNotNull(openButton);
openButton.Click();
var notepadWindow = this.Find<Window>($"{TestFileName} - Notepad", global: true);
FindDefaultAppDialogAndClickButton();
var notepadWindow = FindNotepadWindow(TestFileBaseName, global: true);
Assert.IsNotNull(notepadWindow);
}
@@ -88,7 +93,9 @@ public class IndexerTests : CommandPaletteTestBase
searchItem.DoubleClick();
var notepadWindow = this.Find<Window>($"{TestFileName} - Notepad", global: true);
FindDefaultAppDialogAndClickButton();
var notepadWindow = FindNotepadWindow(TestFileBaseName, global: true);
Assert.IsNotNull(notepadWindow);
}
@@ -107,9 +114,9 @@ public class IndexerTests : CommandPaletteTestBase
Assert.IsNotNull(openButton);
openButton.Click();
var notepadWindow = this.Find<Window>($"{TestFolderName} - File Explorer", global: true);
var fileExplorer = FindExplorerWindow(TestFolderName, global: true);
Assert.IsNotNull(notepadWindow);
Assert.IsNotNull(fileExplorer);
}
[TestMethod]
@@ -122,7 +129,7 @@ public class IndexerTests : CommandPaletteTestBase
Assert.IsNotNull(searchItem);
searchItem.DoubleClick();
var fileExplorer = this.Find<Window>($"{TestFolderName} - File Explorer", global: true);
var fileExplorer = FindExplorerWindow(TestFolderName, global: true);
Assert.IsNotNull(fileExplorer);
}
@@ -181,7 +188,7 @@ public class IndexerTests : CommandPaletteTestBase
Assert.IsNotNull(showInFolderButton);
showInFolderButton.Click();
var fileExplorer = this.Find<Window>($"{TestFolderName} - File Explorer", global: true, timeoutMS: 20000);
var fileExplorer = FindExplorerWindow(TestFolderName, global: true, timeoutMS: 20000);
Assert.IsNotNull(fileExplorer);
}
@@ -201,7 +208,7 @@ public class IndexerTests : CommandPaletteTestBase
Assert.IsNotNull(copyPathButton);
copyPathButton.Click();
var textItem = this.Find<Window>("C:\\Windows\\system32\\cmd.exe", global: true);
var textItem = FindByPartialName("C:\\Windows\\system32\\cmd.exe", global: true);
Assert.IsNotNull(textItem, "The console did not open with the expected path.");
}
@@ -220,7 +227,7 @@ public class IndexerTests : CommandPaletteTestBase
Assert.IsNotNull(copyPathButton);
copyPathButton.Click();
var propertiesWindow = this.Find<Window>($"{TestFileName} Properties", global: true);
var propertiesWindow = FindByClassNameAndNamePattern<Window>("#32770", "Properties", global: true);
Assert.IsNotNull(propertiesWindow, "The properties window did not open for the selected file.");
}
}

View File

@@ -44,25 +44,25 @@ public class AllAppsSettings : JsonSettingsManager
private readonly ToggleSetting _enableStartMenuSource = new(
Namespaced(nameof(EnableStartMenuSource)),
Resources.enable_start_menu_source,
Resources.enable_start_menu_source,
string.Empty,
true);
private readonly ToggleSetting _enableDesktopSource = new(
Namespaced(nameof(EnableDesktopSource)),
Resources.enable_desktop_source,
Resources.enable_desktop_source,
string.Empty,
true);
private readonly ToggleSetting _enableRegistrySource = new(
Namespaced(nameof(EnableRegistrySource)),
Resources.enable_registry_source,
Resources.enable_registry_source,
string.Empty,
false); // This one is very noisy
private readonly ToggleSetting _enablePathEnvironmentVariableSource = new(
Namespaced(nameof(EnablePathEnvironmentVariableSource)),
Resources.enable_path_environment_variable_source,
Resources.enable_path_environment_variable_source,
string.Empty,
false); // this one is very VERY noisy
public double MinScoreThreshold { get; set; } = 0.75;

View File

@@ -12,21 +12,21 @@ namespace Microsoft.CmdPal.Ext.Calc;
public partial class CalculatorCommandProvider : CommandProvider
{
private static ISettingsInterface settings = new SettingsManager();
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
{
Subtitle = Resources.calculator_top_level_subtitle,
MoreCommands = [new CommandContextItem(settings.Settings.SettingsPage)],
MoreCommands = [new CommandContextItem(((SettingsManager)settings).Settings.SettingsPage)],
};
private readonly FallbackCalculatorItem _fallback = new(settings);
private static SettingsManager settings = new();
public CalculatorCommandProvider()
{
Id = "Calculator";
DisplayName = Resources.calculator_display_name;
Icon = Icons.CalculatorIcon;
Settings = settings.Settings;
Settings = ((SettingsManager)settings).Settings;
}
public override ICommandItem[] TopLevelCommands() => [_listItem];

View File

@@ -34,7 +34,7 @@ public static class CalculateEngine
/// Interpret
/// </summary>
/// <param name="cultureInfo">Use CultureInfo.CurrentCulture if something is user facing</param>
public static CalculateResult Interpret(SettingsManager settings, string input, CultureInfo cultureInfo, out string error)
public static CalculateResult Interpret(ISettingsInterface settings, string input, CultureInfo cultureInfo, out string error)
{
error = default;

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Calc.Helper;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public interface ISettingsInterface
{
public CalculateEngine.TrigMode TrigUnit { get; }
public bool InputUseEnglishFormat { get; }
public bool OutputUseEnglishFormat { get; }
public bool CloseOnEnter { get; }
}

View File

@@ -12,7 +12,7 @@ namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static partial class QueryHelper
{
public static ListItem Query(string query, SettingsManager settings, bool isFallbackSearch, TypedEventHandler<object, object> handleSave = null)
public static ListItem Query(string query, ISettingsInterface settings, bool isFallbackSearch, TypedEventHandler<object, object> handleSave = null)
{
ArgumentNullException.ThrowIfNull(query);
if (!isFallbackSearch)

View File

@@ -13,7 +13,7 @@ namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class ResultHelper
{
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, SettingsManager settings, TypedEventHandler<object, object> handleSave)
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, ISettingsInterface settings, TypedEventHandler<object, object> handleSave)
{
// Return null when the expression is not a valid calculator query.
if (roundedResult == null)

View File

@@ -8,7 +8,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public class SettingsManager : JsonSettingsManager
public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
private static readonly string _namespace = "calculator";

View File

@@ -23,7 +23,7 @@ namespace Microsoft.CmdPal.Ext.Calc.Pages;
public sealed partial class CalculatorListPage : DynamicListPage
{
private readonly Lock _resultsLock = new();
private readonly SettingsManager _settingsManager;
private readonly ISettingsInterface _settingsManager;
private readonly List<ListItem> _items = [];
private readonly List<ListItem> history = [];
private readonly ListItem _emptyItem;
@@ -32,7 +32,7 @@ public sealed partial class CalculatorListPage : DynamicListPage
// We need to avoid the double calculation. This may cause some wierd behaviors.
private string skipQuerySearchText = string.Empty;
public CalculatorListPage(SettingsManager settings)
public CalculatorListPage(ISettingsInterface settings)
{
_settingsManager = settings;
Icon = Icons.CalculatorIcon;

View File

@@ -11,9 +11,9 @@ namespace Microsoft.CmdPal.Ext.Calc.Pages;
public sealed partial class FallbackCalculatorItem : FallbackCommandItem
{
private readonly CopyTextCommand _copyCommand = new(string.Empty);
private readonly SettingsManager _settings;
private readonly ISettingsInterface _settings;
public FallbackCalculatorItem(SettingsManager settings)
public FallbackCalculatorItem(ISettingsInterface settings)
: base(new NoOpCommand(), Resources.calculator_title)
{
Command = _copyCommand;

View File

@@ -85,6 +85,10 @@ internal sealed partial class IndexerListItem : ListItem
commands.Add(new CommandContextItem(openCommand));
}
}
else
{
commands.Add(new CommandContextItem(openCommand));
}
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }));

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
public interface ISettingsInterface
{
// Add registry-specific settings methods here if needed
// For now, this can be empty if there are no settings for Registry
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
private static readonly string _namespace = "registry";
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
internal static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
// Add settings here when needed
// Settings.Add(setting);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
}

View File

@@ -18,12 +18,14 @@ internal sealed partial class RegistryListPage : DynamicListPage
public static IconInfo RegistryIcon { get; } = new("\uE74C"); // OEM
private readonly CommandItem _emptyMessage;
private readonly ISettingsInterface _settingsManager;
public RegistryListPage()
public RegistryListPage(ISettingsInterface settingsManager)
{
Icon = Icons.RegistryIcon;
Name = Title = Resources.Registry_Page_Title;
Id = "com.microsoft.cmdpal.registry";
_settingsManager = settingsManager;
_emptyMessage = new CommandItem()
{
Icon = Icons.RegistryIcon,

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Registry.Helpers;
using Microsoft.CmdPal.Ext.Registry.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -10,6 +11,8 @@ namespace Microsoft.CmdPal.Ext.Registry;
public partial class RegistryCommandsProvider : CommandProvider
{
private static readonly ISettingsInterface _settingsManager = new SettingsManager();
public RegistryCommandsProvider()
{
Id = "Windows.Registry";
@@ -20,7 +23,7 @@ public partial class RegistryCommandsProvider : CommandProvider
public override ICommandItem[] TopLevelCommands()
{
return [
new CommandItem(new RegistryListPage())
new CommandItem(new RegistryListPage(_settingsManager))
{
Title = "Registry",
Subtitle = "Navigate the Windows registry",

View File

@@ -16,10 +16,10 @@ namespace Microsoft.CmdPal.Ext.TimeDate;
internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
{
private readonly HashSet<string> _validOptions;
private SettingsManager _settingsManager;
private ISettingsInterface _settingsManager;
private DateTime? _timestamp;
public FallbackTimeDateItem(SettingsManager settings, DateTime? timestamp = null)
public FallbackTimeDateItem(ISettingsInterface settings, DateTime? timestamp = null)
: base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title)
{
Title = string.Empty;

View File

@@ -22,7 +22,7 @@ internal static class AvailableResultsList
/// <param name="firstWeekOfYear">Required for UnitTest: Use custom first week of the year instead of the plugin setting.</param>
/// <param name="firstDayOfWeek">Required for UnitTest: Use custom first day of the week instead the plugin setting.</param>
/// <returns>List of results</returns>
internal static List<AvailableResult> GetList(bool isKeywordSearch, SettingsManager settings, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null)
internal static List<AvailableResult> GetList(bool isKeywordSearch, ISettingsInterface settings, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null)
{
var results = new List<AvailableResult>();
var calendar = CultureInfo.CurrentCulture.Calendar;

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Ext.TimeDate.Helpers;
public interface ISettingsInterface
{
public int FirstWeekOfYear { get; }
public int FirstDayOfWeek { get; }
public bool EnableFallbackItems { get; }
public bool TimeWithSecond { get; }
public bool DateWithWeekday { get; }
public List<string> CustomFormats { get; }
}

View File

@@ -11,7 +11,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.TimeDate.Helpers;
public class SettingsManager : JsonSettingsManager
public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
// Line break character used in WinUI3 TextBox and TextBlock.
private const char TEXTBOXNEWLINE = '\r';

View File

@@ -27,7 +27,7 @@ public sealed partial class TimeDateCalculator
/// </summary>
/// <param name="query">Search query object</param>
/// <returns>List of Wox <see cref="Result"/>s.</returns>
public static List<ListItem> ExecuteSearch(SettingsManager settings, string query)
public static List<ListItem> ExecuteSearch(ISettingsInterface settings, string query)
{
var isEmptySearchInput = string.IsNullOrWhiteSpace(query);
List<AvailableResult> availableFormats = new List<AvailableResult>();

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