Compare commits

...

83 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
aa262cdf20 update ut 2025-12-22 16:11:25 +08:00
Shawn Yuan (from Dev Box)
bb3dee489b fix bug 2025-12-22 15:22:27 +08:00
Shawn Yuan (from Dev Box)
01aa491962 fix ut 2025-12-22 12:48:37 +08:00
Shawn Yuan (from Dev Box)
ee0d931f39 fix compiling issue 2025-12-22 12:22:47 +08:00
Shawn Yuan (from Dev Box)
eceb55ca9f Merge branch 'main' into shawn/quickAccessImprove 2025-12-22 09:38:43 +08:00
Jessica Dene Earley-Cha
37bd24db36 Cmdpal: user research on extension invokes (#43905)
## Summary of the Pull Request

Added two events that Niels asked for:

- `CmdPal_ExtensionInvoked`
- Track which extensions are being used (e.g., file search, app
launching, calculator, etc.).
    - Properties logged
        - ExtensionId - Unique identifier of the extension provider
        - CommandType - Display name of the command being invoked
        - Success - Whether the command executed successfully
        - ExecutionTimeMs - Execution time in milliseconds
- `CmdPal_SessionDuration`
    - Tracks how long Command Palette stays open (launch → close). 
    - Properties logged
        - DurationMs - Session duration in milliseconds
        - CommandsExecuted - Number of commands executed
        - PagesVisited - Number of pages visited
- DismissalReason - Why the session ended (Escape, LostFocus, Command,
etc.)
        - SearchQueriesCount - Number of search queries executed
        - MaxNavigationDepth - Maximum navigation depth reached
        - ErrorCount - Number of errors encountered



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

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
2025-12-19 11:23:16 -08:00
Kai Tao
557a07589d Fix spell check errors (#44358)
<!-- 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
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
No spell check error appears
2025-12-19 17:03:27 +08:00
Shawn Yuan
2603efc8a9 Fix pipeline with waskd2 experimental version (#44357)
<!-- 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 includes two main groups of changes: improvements to
build configuration (specifically, warning suppression for experimental
features), and refactoring in the `WrapPanelCustom` control to use the
correct `StretchChild` type from the CommunityToolkit. These updates
help streamline the build process and improve code clarity and
maintainability.

**Build configuration improvements:**

* Added `/p:IgnoreExperimentalWarnings=true` to the
`RestoreAdditionalProjectSourcesArg` in the build pipeline to enable
suppression of experimental warnings during project restore.
* Updated `Directory.Build.targets` to append specific warning codes
(`CS8305`, `SA1500`, `CA1852`) to the `NoWarn` property when
`IgnoreExperimentalWarnings` is set, suppressing these warnings during
builds.

**Refactoring in `WrapPanelCustom`:**

* Updated the `WrapPanel` control in `WrapPanel.cs` to use
`ToolkitStretchChild` from `CommunityToolkit.WinUI.Controls` instead of
a local `StretchChild` type, ensuring consistency and leveraging the
toolkit's implementation.
[[1]](diffhunk://#diff-53e4c6a2890b6fb1c66ea8a19f0049070910083608f8fb80c2b1f5a8d254d007R11-R12)
[[2]](diffhunk://#diff-53e4c6a2890b6fb1c66ea8a19f0049070910083608f8fb80c2b1f5a8d254d007L180-R184)
[[3]](diffhunk://#diff-53e4c6a2890b6fb1c66ea8a19f0049070910083608f8fb80c2b1f5a8d254d007L193-R197)
[[4]](diffhunk://#diff-53e4c6a2890b6fb1c66ea8a19f0049070910083608f8fb80c2b1f5a8d254d007L400-R402)
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-19 16:02:17 +08:00
leileizhang
dd138fb94b FancyZones CLI: migrate to System.CommandLine and centralize data I/O (#44344)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This PR refactors FancyZones CLI to use System.CommandLine and
consolidates all FancyZones JSON config read/write through
FancyZonesEditorCommon data models and FancyZonesDataIO, so CLI and
Editor share the same serialization and file paths.

For detailed command definitions, see PR #44078
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-19 11:53:43 +08:00
Noraa Junker
7cd201d355 Remove ISettingsUtils and ISettingsPath interfaces (#44331)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR removes the ISettingsUtils and ISettingsPath interfaces to
reduce some complexity. They only existed so the classes can be used
with moq. But this is also possible by using virtual methods which is
cleaner.

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

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [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
2025-12-19 10:30:01 +08:00
Shawn Yuan
9aab0f3893 Fix pipeline with waskd2 exp (#44334)
<!-- 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 updates the WinAppSDK version used in the build
pipeline to 1.8 and makes related improvements to the NuGet restore
process and configuration handling.

Version update:

* Updated the default value of the `winAppSdkVersionNumber` parameter
from `"1.7"` to `"1.8"` in `.pipelines/UpdateVersions.ps1`, ensuring the
pipeline uses the latest WinAppSDK version.

NuGet restore and configuration improvements:

* Changed the `Add-NuGetSourceAndMapping` call in the
`Resolve-WinAppSdkSplitDependencies` function to use the `$installDir`
variable instead of a hardcoded path, improving flexibility for local
package mapping in `.pipelines/UpdateVersions.ps1`.
* Added a `workingDirectory` property to the NuGet restore step in
`.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml`
to ensure the restore process operates from the correct directory.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-18 14:47:59 +08:00
Shawn Yuan
91b7a99e76 Update BuildWithLatestWinAppSdkDaily pipeline to use 1.8 wasdk (#44183)
<!-- 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 refactors the WindowsAppSDK update and NuGet restore
pipeline to improve dependency resolution and configuration management,
especially for version 1.8 and above. The changes streamline how package
versions are detected and updated across multiple project files,
introduce more robust handling of NuGet sources and mappings, and
modernize the restore step to use the `dotnet` CLI for better
compatibility.

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

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

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

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

---------

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-18 11:39:35 +08:00
Gordon Lam
d38edf798d Update a reminding vcxproj that reference WinAppSDK (#44316)
<!-- 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 modernizes NuGet package management for the
`Microsoft.CommandPalette.Extensions` native project by migrating from
the older `packages.config` approach to the newer `PackageReference`
style. It also updates the related project references and output
handling in the toolkit project. These changes simplify dependency
management and align with current best practices for native C++/WinRT
projects.

**NuGet package management modernization:**

* Migrated `Microsoft.CommandPalette.Extensions.vcxproj` from
`packages.config` to `PackageReference` style, specifying dependencies
directly in the project file and removing the `packages.config` file.
[[1]](diffhunk://#diff-ff17a18a84e1c666c8f05468624d55167ac13d2c0e36724e0df3ce1d83bdbbd4L3-L12)
[[2]](diffhunk://#diff-ff17a18a84e1c666c8f05468624d55167ac13d2c0e36724e0df3ce1d83bdbbd4R27-R33)
[[3]](diffhunk://#diff-13e4e73ced13b2508639b5e93c39b0f1ee6a978109c60d33e3a9d16bf24024bfL1-L17)
* Removed legacy NuGet property groups, imports, and error-checking
targets related to manual package restore, as these are now handled
automatically by `PackageReference`.
[[1]](diffhunk://#diff-ff17a18a84e1c666c8f05468624d55167ac13d2c0e36724e0df3ce1d83bdbbd4L3-L12)
[[2]](diffhunk://#diff-ff17a18a84e1c666c8f05468624d55167ac13d2c0e36724e0df3ce1d83bdbbd4L165-L182)

**Project reference and build output updates:**

* Updated the project reference in
`Microsoft.CommandPalette.Extensions.Toolkit.csproj` to not reference
the output assembly directly, and added explicit inclusion and copying
of the native implementation DLL and WinMD files to the output
directory.

**Cleanup of unused files:**

* Removed `packages.config` from both the
`Microsoft.CommandPalette.Extensions` and `PowerRenameUILib` projects,
as dependencies are now managed via `PackageReference`.
[[1]](diffhunk://#diff-13e4e73ced13b2508639b5e93c39b0f1ee6a978109c60d33e3a9d16bf24024bfL1-L17)
[[2]](diffhunk://#diff-98d060eb88d212ec4ce70e1d30ec66043a998d67940648c917aa6609739d10d5L1-L19)
2025-12-17 18:10:21 +08:00
Dave Rayment
17668047bf [PowerRename] Fix date replacement tokens failing if followed by a capital letter (#44267)
## Summary of the Pull Request
Fixes date/time-related replacement tokens being rejected if they were
followed by capital letters in the user's replacement string.

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

- [x] Closes: #44202
- [ ] **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
With the addition of image metadata replacement options, a strict
negative lookahead was added to the date replacement regular expressions
to prevent conflicts. This was required because, for example, `$D` would
otherwise match before `$DATE_TAKEN_YYYY`. Metadata and date-related
replacements are executed separately at the moment, so this awareness of
each other is required.

However, the negative lookahead was far too aggressive:
- It used `(?![A-Z])`, meaning any capital letter after the date token
would reject the match entirely. This caused the problem referred to in
the linked issue, where `$DDT` was rejected instead of matching to the
`$DD` replacement followed by a verbatim `T` character.
- It was applied to the majority of fields, whereas it is only actually
needed where date tokens are prefixes of metadata tokens. Only `$D` and
`$H` are affected.
- There was no need to apply negative lookups to catch 'self-matches'
like preventing `$D`, `$DD`, and `$DDD` from matching when `$DDDD` was
in the replacement string. Instead, the order of processing already
matches the longest token first, so this could never happen.

To fix these issues, I:
- Removed the majority of the negative lookaheads.
- Made the remaining negative lookaheads only match actual conflicting
suffixes, e.g. `$D(?!(ATE_TAKEN_|ESCRIPTION|OCUMENT_ID))` instead of
`$D(?![A-Z])`. This makes mistaken rejections of user-supplied
replacement strings much more unlikely.

Please note: there remain inherent issues with the current token
replacement approach. Tackling these will require a more extensive
refactoring PR which separates replacement string tokenization from
matching and replacement, and which tackles both image metadata and file
date metadata in a unified manner.

## Validation Steps Performed
- Corrected unit tests which classified, for example, `$YYY` as invalid,
instead of identifying it as a valid `$YY` token + verbatim capital Y
replacement.
- Wrote new unit tests to exercise the refined negative lookaheads.
- Wrote tests to confirm certain negative lookaheads were not required
because the order of processing guaranteed the longest match happens
before any prefix matches.
- Wrote a unit test to exercise the specific issue raised in #44202.
- Confirmed that all new and pre-existing PowerRename tests pass.
2025-12-17 16:54:58 +08:00
Shawn Yuan (from Dev Box)
e941ea4ebf Merge branch 'main' into shawn/quickAccessImprove 2025-12-17 13:26:54 +08:00
Shawn Yuan
7b0b284d40 [Advanced Paste] Introduced image-input handling (#44021)
<!-- 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 significant enhancements to the
AdvancedPaste module, enabling AI-powered clipboard transformations to
support both text and image data (notably for image analysis and
transformation tasks), and improving error handling and clipboard
tracking. The changes update the service interfaces, data models, and
processing logic to handle images alongside text, and refine how the
application responds to errors and clipboard state changes.

<img width="470" height="366" alt="image"
src="https://github.com/user-attachments/assets/6ad011e4-a2ba-4e44-b640-739440836de6"
/>

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

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

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

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

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-17 11:49:28 +08:00
Gordon Lam
9aca6d136f Revert "Revert commit" - Using centralized package management for vcxproj (#44289)
Reverts microsoft/PowerToys#44208
Basically enable back: https://github.com/microsoft/PowerToys/pull/43920

the core change is adding this new Target to ensure when "building in
Visual Studio", it will restore the nuget package first for vcxproj:
```xml
  <!-- Auto-restore NuGet for native vcxproj (PackageReference) when building inside VS -->
  <Target Name="EnsureNuGetRestoreForVcxproj" BeforeTargets="PrepareForBuild" Condition="
            '$(BuildingInsideVisualStudio)' == 'true'
            and '$(DesignTimeBuild)' != 'true'
            and '$(RestoreInProgress)' != 'true'
            and '$(MSBuildProjectExtension)' == '.vcxproj'
            and '$(RestoreProjectStyle)' == 'PackageReference'
            and '$(MSBuildProjectExtensionsPath)' != ''
            and !Exists('$(MSBuildProjectExtensionsPath)project.assets.json')
          ">

    <Message Importance="normal" Text="NuGet assets missing for $(MSBuildProjectName); running Restore...; IntDir=$(IntDir); BaseIntermediateOutputPath=$(BaseIntermediateOutputPath)" />

    <MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Restore" Properties="RestoreInProgress=true" BuildInParallel="false" />
  </Target>
```
2025-12-16 10:46:39 +08:00
leileizhang
4b2ee60b42 Fix AdvancedPaste Gemini provider saving Azure placeholder endpoint (#44293)
<!-- 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
When creating/editing a Paste AI provider in Settings, providers that
don’t require an endpoint (e.g., Google/Gemini) could still end up
persisting the Azure OpenAI placeholder
(https://your-resource.openai.azure.com/) into settings.json.

### Fix:

- On save, for service types that don’t use an endpoint, prevent
placeholder/stale values from being persisted by forcing endpoint-url to
be empty.
- Reuse a single “requires endpoint” check so the dialog visibility and
save behavior stay consistent.

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

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

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

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

## AI Generated Note:
This pull request refactors and improves how endpoint handling is
managed for different AI service types in the
`AdvancedPastePage.xaml.cs` file. The changes centralize the logic for
determining whether an endpoint is required, prevent persisting
placeholder or stale endpoint values for services that do not use them,
and ensure placeholder values are only provided where appropriate.

**Refactoring and logic centralization:**

* Introduced the `RequiresEndpointForService` helper method to
centralize and clarify the logic for determining if a service type
requires an endpoint, replacing inline checks in multiple places.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36L317-R317)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R838-R845)

**Improved endpoint value handling:**

* Updated the dialog logic to ensure that endpoints are not persisted
for services that do not require them, preventing storage of placeholder
or irrelevant values.
* Modified the `GetEndpointPlaceholder` method to return an empty string
for service types that do not require an endpoint, rather than a generic
placeholder.
2025-12-15 17:07:09 +08:00
Shawn Yuan
e37a328624 [Advanced Paste] Fixed custom hotkey issue (#44288)
<!-- 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 custom hotkey issue

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

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

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

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

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-15 12:56:35 +08:00
Shawn Yuan
b1cf7fa9da Merge branch 'main' into shawn/quickAccessImprove 2025-12-15 10:28:34 +08:00
moooyo
66e96bbe9d Super resolution with AI for image resizer (#42331)
<!-- 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
From WinAppSDK 1.8, microsoft announced a new feature AI Imaging. We can
use this ability to enhance our image resizer tools to support scale up
the image resolution by AI.

Doc:
https://learn.microsoft.com/en-us/windows/ai/apis/imaging#what-can-i-do-with-image-super-resolution

Target:
1. Add a new config to control use AI or not.
2. Support model download in image resizer.
3. Auto detect if user's computer support AI feature, if not, do not
show the AI related config.
4. Switch the control part if user enable/disable ai feature.

Demo:
Model not ready, user need to download the model:
<img width="694" height="625" alt="image"
src="https://github.com/user-attachments/assets/8079f047-71fa-4abf-b266-003f74cc5d3e"
/>

Model ready:
<img width="543" height="589" alt="image"
src="https://github.com/user-attachments/assets/952eafc6-0af6-4bea-88d0-0724532f4fac"
/>

User's computer doesn't support AI feature (x86 machine)
<img width="685" height="531" alt="image"
src="https://github.com/user-attachments/assets/522ba300-1505-46a2-a29b-3e8e71c49cdd"
/>


Note: **This feature only support for Arm Windows with the latest
Windows version.**


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

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

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

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

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: moooyo <lengyuchn@gmail.com>
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-12-15 09:42:49 +08:00
Jiří Polášek
e13d6a78aa CmdPal: Set image to surface immediately in BlurImageControl (#44222)
## Summary of the Pull Request

This PR resolves issue when the background image is not loaded. When
loading an image, BlurImageControl now sets the image to the surface
immediately, as the LoadComplete handler is not guaranteed to be called.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-11 19:05:01 -06:00
Jiří Polášek
73786cd2be CmdPal: Add drag & drop support (#44165)
## Summary of the Pull Request

This PR adds basic drag-and-drop support for items in list and grid
views.

It introduces two new properties on `ListItem`, backed by
`IExtendedAttributesProvider`: `DataPackage` and `DataPackageView`.
These properties are mutually exclusive.
`DataPackage` serves as a convenience property allowing the item to
retain the underlying object without risk of losing it. Across the
extension boundary, only the immutable `DataPackageView` snapshot is
transferred. When `DataPackage` is set, `DataPackageView` is derived
from it.

This PR includes initial concrete drag-and-drop implementations for:
- File Indexer  
- Clipboard History  

**Todo / Missing pieces** 
- [x] Extend `DataPackage` support to top-level command items, enabling
scenarios such as index fallback ~
- [x] Provide automatic drag-and-drop for unconfigured list items (e.g.,
copying title and subtitle as text)
- [x] Keep CmdPal open
- [ ] ~Clipboard commands (since we have the DataPackage...)~
- [ ] ~Improve logging~

## Pictures? Moving ones!


https://github.com/user-attachments/assets/13eb9a71-e760-43ea-8c2d-cd41cf377905




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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-11 08:05:48 -06:00
Shawn Yuan (from Dev Box)
346c99543a code clean 2025-12-11 18:39:57 +08:00
Shawn Yuan (from Dev Box)
1211e2c92f QuickAccessList control refactor 2025-12-11 17:57:56 +08:00
Shawn Yuan (from Dev Box)
9f99727bf5 fix merge issue 2025-12-11 16:28:00 +08:00
Shawn Yuan (from Dev Box)
fee146b084 Merge remote-tracking branch 'origin/main' into shawn/quickAccessImprove 2025-12-11 16:18:17 +08:00
leileizhang
4de4d5f310 [Advanced Paste] Fix clipboard history item duplication when selecting items (#44212)
<!-- 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 an issue where clicking on a clipboard history item creates a
duplicate entry at the top of the history.
## Problem

When a user clicks on a clipboard history item in the Advanced Paste
clipboard history menu, the item gets duplicated to the top of the
history. This makes it impossible to delete items since clicking the
three-dots menu to delete first duplicates the item.

## Root Cause

The `ClipboardHistory_ItemInvoked` handler was using
`ClipboardHelper.SetTextContent()` and
`ClipboardHelper.SetImageContent()`, which internally call
`Clipboard.SetContentWithOptions()`. This creates a new clipboard
history entry with the same content.

## Solution

Use `Clipboard.SetHistoryItemAsContent(ClipboardHistoryItem)` instead,
which sets the clipboard content from an existing history item without
creating a new history entry.


![delete](https://github.com/user-attachments/assets/256407e7-a05f-4cb5-9311-de8918940d66)

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-11 14:12:06 +08:00
Kai Tao
f8c5ff8c0c Build: Fix build script for a local installer (#44168)
<!-- 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

Not maintained since wix5 upgrade, so make it build locally for an
installer
1. Do elevation when dev cert is not added to root store
2. Set up version to build arg to build, and build cmdpal version same
with CI
3. cmdpal AOT local build
4. Make sure every msix file is signed successfully

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

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

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

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

Verify the script can build an installer on a new devbox, and cmdpal,
filelocksmith etc can be run without problem
<img width="872" height="275" alt="image"
src="https://github.com/user-attachments/assets/cf4cff0d-0d90-4496-a7f8-50c582d9c340"
/>
<img width="1251" height="555" alt="image"
src="https://github.com/user-attachments/assets/6529c1a8-a532-4dfc-9f74-2c2fd37e28e6"
/>
Output for msix packages:
PackageFullName Version
--------------- -------
Microsoft.PowerToys.SparseApp_0.96.2.0_neutral__8wekyb3d8bbwe 0.96.2.0

Microsoft.PowerToys.FileLocksmithContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0

Microsoft.PowerToys.ImageResizerContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0
Microsoft.PowerToys.NewPlusContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0

Microsoft.PowerToys.PowerRenameContextMenu_0.96.2.0_neutral__8wekyb3d8bbwe
0.96.2.0

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 13:17:42 +08:00
leileizhang
d32ea86314 [Peek] Use WebView2 for SVG preview to improve compatibility (#44209)
<!-- 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
Improves SVG preview compatibility in Peek by using WebView2 instead of
`SvgImageSource`.
## Problem

`SvgImageSource` has limited SVG feature support and fails to render
SVGs with advanced features like `clipPath`, complex gradients, or
certain namespace configurations. This results in black previews or
icon-only display for many SVG files.

## Solution

Render SVG files using WebView2 which provides full SVG specification
support through the browser engine.

<img width="1973" height="1314" alt="image"
src="https://github.com/user-attachments/assets/a4eb2ff5-d76f-4f7f-87e3-6404e18b2b09"
/>

<img width="1997" height="1358" alt="image"
src="https://github.com/user-attachments/assets/7ce4dd69-7fba-447e-8499-d37af3f02c4d"
/>
<!-- Please review the items on the PR checklist before submitting-->

## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-11 11:58:01 +08:00
Shawn Yuan
177f144e6d Revert commit (#44208)
<!-- 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
Revert commit of 06fcbdac40 and
60deec6815
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [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
2025-12-11 11:23:18 +08:00
Kai Tao
7b469f6327 Build: Fix a version check failure (#44199)
<!-- 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
Version check failed for a dll, ignore it to let release pipeline pass

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-11 10:58:30 +08:00
Guilherme
94ace730c8 [CmdPal] Add Sections and Separators for List Pages and Grid Pages (#43952)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This pull request adds sections and separators to ListPages and
GridPages

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

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

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

Since `CollectionViewSource` was causing performance issues and
@zadjii-msft asked for a new approach, I came up with this idea, heavily
inspired by how separators work on the `ContextMenu`, `FiltersDropDown`
and `Details`.

The way this is currently working is: Any ListItem where `Section` is
not null and `Command` is null, is considered a Separator.

On my tests, this seems to be working fine. Tried to make this work
without changes to the API, but I think this needs to be discussed.

### Some of the possible enhancements to existing extensions

### Search apps

<img width="792" height="523" alt="Screenshot 2025-11-27 173618"
src="https://github.com/user-attachments/assets/f9f9a64d-3ec1-4f7e-922b-997a3a4d074d"
/>

### Window Walker

<img width="785" height="518" alt="Screenshot 2025-11-27 173728"
src="https://github.com/user-attachments/assets/230f647d-210a-4b60-9068-c8fff890d2c9"
/>

### Winget

<img width="809" height="497" alt="Screenshot 2025-11-27 174006"
src="https://github.com/user-attachments/assets/547529c1-7600-4438-8c3e-e872e0327650"
/>

### Search files

<img width="819" height="536" alt="image"
src="https://github.com/user-attachments/assets/e86accc0-3f85-412d-8fb0-914a5479baff"
/>

### Grid Pages

<img width="804" height="964" alt="Screenshot 2025-11-27 174055"
src="https://github.com/user-attachments/assets/a3bba7db-95df-47ec-9cfb-f38775ab960e"
/>



<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-10 20:51:42 -06:00
Guilherme
a08fc0921f [CmdPal] Introduce Small, Medium, and Large sizing options for Details (#43956)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This PR introduces 3 new sizing options to the Details Panel in the
Extensions API.
- `Small` (Default)
- `Medium`
- `Large`
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

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

Here's how it looks like:
```csharp
new ListItem(new NoOpCommand())
{
    Title = "Details on ListItems (Medium)",
    Details = new Details()
    {
        Title = "This item has medium details size",
        Body = "Each of these items can have a `Body` formatted with **Markdown**",
        Size = ContentSize.Medium,
    },
},
```

### Moving Pictures


![DetailsSize](https://github.com/user-attachments/assets/ae11b767-ecba-4b39-bd81-3e77eec93ed0)


<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-10 19:25:45 -06:00
Gleb Khmyznikov
995bbdc62d Fix fancy zones UI tests #42249 (#44181)
- [ ] Closes: #42249

Contribution to https://github.com/microsoft/PowerToys/issues/40701
2025-12-10 10:04:04 -08:00
Niels Laute
91cceb70d5 Update AppsListPage.xaml 2025-12-10 15:56:36 +01:00
Niels Laute
b73908c413 More changes 2025-12-10 15:44:01 +01:00
Jaylyn Barbee
f822826cf1 [Light Switch] Follow Night Light mode (#43683)
<!-- 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
Introduces a new mode that will have Light Switch follow Windows Night
Light.

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

- [x] Closes: https://github.com/microsoft/PowerToys/issues/42457
- [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
- [ ] **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
Strictly follows the state of Night Light. When NL is on, LS will be
switch to dark mode and when NL is off, LS will switch to light mode
(with respect to the users system/app selection).

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Turn on Follow Night Light mode
Change night light!

## Notes
2025-12-10 08:15:34 -05:00
Niels Laute
062589b9f7 Minor UX improvements 2025-12-10 12:57:07 +01:00
Shawn Yuan (from Dev Box)
ff21b77b7b bug fixing
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-10 11:39:40 +08:00
Shawn Yuan (from Dev Box)
d6ae07c040 Merge branch 'shawn/quickAccessImprove' of https://github.com/microsoft/PowerToys into shawn/quickAccessImprove 2025-12-10 10:49:31 +08:00
Shawn Yuan (from Dev Box)
e038a09357 fix navigation issue
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-10 10:48:36 +08:00
Jiří Polášek
97c1de8bf6 CmdPal: Light, dark, pink, and unicorns (#43505)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces user settings for app mode themes (dark, light, or
system) and background customization options, including custom colors,
system accent colors, or custom images.

- Adds a new page to the Settings window with new appearance settings
and moves some existing settings there as well.
- Introduces a new core-level service abstraction, `IThemeService`, that
holds the state for the current theme.
- Uses the helper class `ResourceSwapper` to update application-level
XAML resources. The way WinUI / XAML handles these is painful, and XAML
Hot Reload is pain². Initialization must be lazy, as XAML resources can
only be accessed after the window is activated.
- `ThemeService` takes app and system settings and selects one of the
registered `IThemeProvider`s to calculate visuals and choose the
appropriate XAML resources.
  - At the moment, there are two:
    - `NormalThemeProvider`
      - Provides the current uncolorized light and dark styles  
      - `ms-appx:///Styles/Theme.Normal.xaml`
    - `ColorfulThemeProvider`
- Style that matches the Windows 11 visual style (based on the Start
menu) and colors
      - `ms-appx:///Styles/Theme.Colorful.xaml`  
- Applied when the background is colorized or a background image is
selected
- The app theme is applied only on the main window
(`WindowThemeSynchronizer` helper class can be used to synchronize other
windows if needed).
- Adds a new dependency on `Microsoft.Graphics.Win2D`.
- Adds a custom color picker popup; the one from the Community Toolkit
occasionally loses the selected color.
- Flyby: separates the keyword tag and localizable label for pages in
the Settings window navigation.


## Pictures? Pictures!

<img width="2027" height="1276" alt="image"
src="https://github.com/user-attachments/assets/e3485c71-7faa-495b-b455-b313ea6046ee"
/>

<img width="3776" height="2025" alt="image"
src="https://github.com/user-attachments/assets/820fa823-34d4-426d-b066-b1049dc3266f"
/>

Matching Windows accent color and tint:

<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/65f3b608-e282-4894-b7c8-e014a194f11f"
/>


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

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

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

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

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-12-09 18:56:03 -06:00
Niels Laute
f6e6fe676a UX improvements 2025-12-09 19:59:13 +01:00
Kayla Cinnamon
0f8cf94d90 Update Community file (#44175)
<!-- 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
Moved myself to community since I'm now in a new role :)

It's been a blast being on this team <3

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

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

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

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

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
2025-12-09 13:04:09 -05:00
Niels Laute
620f67a3ba [UX] Misc consistency improvements in Settings (#44174)
## Summary of the Pull Request

- Minor text changes (e.g. removing "Enable")
- Fixing a few bugs where textblocks did not look disabled
- Sorted mouse utils alphabetically
- Auto-collapsing expanders on the mouse utils to reduce visual clutter

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-09 17:50:45 +01:00
Shawn Yuan (from Dev Box)
debf4322df fix bug
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-09 14:58:15 +08:00
Shawn Yuan (from Dev Box)
4a823320c4 Implemented shortcut
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-09 14:46:56 +08:00
Kai Tao
23bc278cc8 Cmdpal: Fix cmdpal toolkit restore failure for slnx in release pipeline (#44152)
<!-- 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
Error from pipeline: 

Invalid input 'PowerToys.slnx'. The file type was not recognized.
MSBuild version 17.14.23+b0019275e for .NET Framework
Build started 12/8/2025 6:33:14 AM.

Nuget support for slnx will be ready in nuget version 7, so use msbuild
to restore

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

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

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

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

<img width="606" height="681" alt="image"
src="https://github.com/user-attachments/assets/ca0486a7-0c07-4fad-a81e-b98767ea8005"
/>
2025-12-09 14:22:18 +08:00
Shawn Yuan (from Dev Box)
e89d6d8f5a fix ipc issue
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-08 16:53:21 +08:00
Shawn Yuan (from Dev Box)
2ca7531ae7 update
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-08 16:21:44 +08:00
Shawn Yuan (from Dev Box)
6ba33b0d91 Merge branch 'main' into shawn/quickAccessImprove 2025-12-08 09:37:11 +08:00
Shawn Yuan (from Dev Box)
273eebda25 merge main
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-05 14:24:04 +08:00
Shawn Yuan (from Dev Box)
fdd34bca09 Merge branch 'main' into shawn/quickAccessImprove 2025-12-05 13:57:51 +08:00
Shawn Yuan (from Dev Box)
dd9643dd38 update
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-05 11:08:40 +08:00
Shawn Yuan (from Dev Box)
eb69549320 improve memory usage
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-05 08:48:48 +08:00
Shawn Yuan (from Dev Box)
8f6bd72679 update showing location
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-04 16:21:55 +08:00
Shawn Yuan (from Dev Box)
4865b44de1 fix scrolling issue
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-04 15:47:54 +08:00
Shawn Yuan (from Dev Box)
3381e82fc1 fixed crash issue.
fixed sorting string display issue.
fixed status update issue

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-04 13:26:11 +08:00
Niels Laute
a40be6f9be Some refactoring and removal of redundant code 2025-12-03 15:31:03 +01:00
Shawn Yuan (from Dev Box)
90e4f1ca41 update expect.txt
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-02 13:43:43 +08:00
Shawn Yuan (from Dev Box)
1fcb8e7515 update
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-02 13:33:14 +08:00
Shawn Yuan (from Dev Box)
3ed9641072 update sln
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-02 12:38:10 +08:00
Shawn Yuan (from Dev Box)
0428cf45af Added bug report tool for QA
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-02 11:15:12 +08:00
Shawn Yuan (from Dev Box)
5fc0ae7e42 remove flyout code
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-02 11:03:25 +08:00
Shawn Yuan (from Dev Box)
8cdd8574d6 Merge branch 'main' into shawn/quickAccessImprove 2025-12-01 10:47:24 +08:00
Shawn Yuan (from Dev Box)
f564cf9189 update expect.txt
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-01 10:39:49 +08:00
Shawn Yuan (from Dev Box)
e826cfb491 add expect word
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-01 10:34:18 +08:00
Shawn Yuan (from Dev Box)
45e94f2ce0 folder refactor
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-01 10:20:10 +08:00
Shawn Yuan (from Dev Box)
b3b99d6d11 move controls to lib
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-12-01 09:37:47 +08:00
Shawn Yuan (from Dev Box)
bb180a166a format xaml style
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-25 15:32:24 +08:00
Shawn Yuan (from Dev Box)
7e62c76c18 create winui3 control library
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-25 15:31:19 +08:00
Shawn Yuan (from Dev Box)
68dd7a46f0 fix window hidding issue
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-24 13:24:35 +08:00
Shawn Yuan (from Dev Box)
a67326d86d add sort feature
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-24 13:12:37 +08:00
Shawn Yuan (from Dev Box)
16733718c3 add status sync
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-24 11:22:54 +08:00
Shawn Yuan (from Dev Box)
5bf797d9f6 update xaml style
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-24 10:15:19 +08:00
Shuai Yuan
8de7110918 update
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-11-20 15:33:02 +08:00
Shuai Yuan
ed254b60cc update
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-11-20 15:32:32 +08:00
Shuai Yuan
ea0a13be3a update
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-11-19 14:40:02 +08:00
Shuai Yuan
8e6f40ffe9 Merge branch 'main' into shawn/quickAccessImprove 2025-11-19 12:02:12 +08:00
Shawn Yuan (from Dev Box)
891dd41cc2 hide from taskbar
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-05 17:34:18 +08:00
Shawn Yuan (from Dev Box)
d7e727fa1a update arch
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-05 17:10:27 +08:00
Shawn Yuan (from Dev Box)
709ceed137 init
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-05 11:59:08 +08:00
366 changed files with 15913 additions and 3500 deletions

View File

@@ -335,3 +335,7 @@ azp
feedbackhub
needinfo
reportbug
#ffmpeg
crf
nostdin

View File

@@ -141,8 +141,11 @@ BITSPIXEL
bla
BLACKFRAME
BLENDFUNCTION
blittable
Blockquotes
blt
bluelightreduction
bluelightreductionstate
BLURBEHIND
BLURREGION
bmi
@@ -221,7 +224,6 @@ clientside
CLIPBOARDUPDATE
CLIPCHILDREN
CLIPSIBLINGS
CLITo
closesocket
clp
CLSCTX
@@ -250,6 +252,7 @@ colorformat
colorhistory
colorhistorylimit
COLORKEY
colorref
comctl
comdlg
comexp
@@ -321,7 +324,6 @@ CUSTOMFORMATPLACEHOLDER
CVal
cvd
CVirtual
CVS
CWMO
CXSCREEN
CXSMICON
@@ -361,6 +363,7 @@ DEFAULTICON
defaultlib
DEFAULTONLY
DEFAULTTONEAREST
Defaulttonearest
DEFAULTTONULL
DEFAULTTOPRIMARY
DEFERERASE
@@ -854,6 +857,7 @@ lastcodeanalysissucceeded
LASTEXITCODE
LAYOUTRTL
lbl
Lbuttondown
LCh
lcid
LCIDTo
@@ -974,6 +978,7 @@ maxversiontested
mber
MBM
MBR
Mbuttondown
MDICHILD
MDL
mdtext
@@ -1074,7 +1079,6 @@ MVVMTK
MWBEx
MYICON
NAMECHANGE
Notavailable
namespaceanddescendants
nao
NCACTIVATE
@@ -1113,6 +1117,7 @@ NEWPLUSSHELLEXTENSIONWIN
newrow
nicksnettravels
NIF
nightlight
NLog
NLSTEXT
NMAKE
@@ -1426,6 +1431,7 @@ RAWINPUTHEADER
RAWMODE
RAWPATH
rbhid
Rbuttondown
rclsid
RCZOOMIT
remotedesktop
@@ -1725,6 +1731,7 @@ svgz
SVSI
SWFO
SWP
Swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
@@ -1744,6 +1751,7 @@ syskeydown
SYSKEYUP
SYSLIB
SYSMENU
Sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
@@ -1800,7 +1808,6 @@ tlbimp
tlc
tmain
TNP
toolgood
Toolhelp
toolwindow
TOPDOWNDIB
@@ -1849,6 +1856,8 @@ uitests
UITo
ULONGLONG
ums
UMax
UMin
uncompilable
UNCPRIORITY
UNDNAME
@@ -1862,6 +1871,7 @@ unittests
UNLEN
UNORM
unremapped
Unsubscribes
unvirtualized
unwide
unzoom
@@ -1946,7 +1956,7 @@ vswhere
Vtbl
WANTNUKEWARNING
WANTPALM
wasdk
WASDK
wbem
WBounds
Wca
@@ -2067,6 +2077,7 @@ Wwanpp
xap
XAxis
XButton
Xbuttondown
xclip
xcopy
XDeployment

View File

@@ -353,6 +353,11 @@
"Microsoft.SemanticKernel.Connectors.Ollama.dll",
"OllamaSharp.dll",
"boost_regex-vc143-mt-gd-x32-1_87.dll",
"boost_regex-vc143-mt-gd-x64-1_87.dll",
"boost_regex-vc143-mt-x32-1_87.dll",
"boost_regex-vc143-mt-x64-1_87.dll",
"UnitsNet.dll",
"UtfUnknown.dll",
"Wpf.Ui.dll"

View File

@@ -1,7 +1,7 @@
Param(
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
[string]$winAppSdkVersionNumber = "1.7",
[string]$winAppSdkVersionNumber = "1.8",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
@@ -16,32 +16,7 @@ Param(
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
)
function Update-NugetConfig {
param (
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
)
Write-Host "Updating nuget.config file"
[xml]$xml = Get-Content -Path $filePath
# Add localpackages source into nuget.config
$packageSourcesNode = $xml.configuration.packageSources
$addNode = $xml.CreateElement("add")
$addNode.SetAttribute("key", "localpackages")
$addNode.SetAttribute("value", "localpackages")
$packageSourcesNode.AppendChild($addNode) | Out-Null
# Remove <packageSourceMapping> tag and its content
$packageSourceMappingNode = $xml.configuration.packageSourceMapping
if ($packageSourceMappingNode) {
$xml.configuration.RemoveChild($packageSourceMappingNode) | Out-Null
}
# print nuget.config after modification
$xml.OuterXml
# Save the modified nuget.config file
$xml.Save($filePath)
}
function Read-FileWithEncoding {
param (
@@ -71,6 +46,132 @@ function Write-FileWithEncoding {
$writer.Close()
}
function Add-NuGetSourceAndMapping {
param (
[xml]$Xml,
[string]$Key,
[string]$Value,
[string[]]$Patterns
)
# Ensure packageSources exists
if (-not $Xml.configuration.packageSources) {
$Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null
}
$sources = $Xml.configuration.packageSources
# Add/Update Source
$sourceNode = $sources.SelectSingleNode("add[@key='$Key']")
if (-not $sourceNode) {
$sourceNode = $Xml.CreateElement("add")
$sourceNode.SetAttribute("key", $Key)
$sources.AppendChild($sourceNode) | Out-Null
}
$sourceNode.SetAttribute("value", $Value)
# Ensure packageSourceMapping exists
if (-not $Xml.configuration.packageSourceMapping) {
$Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null
}
$mapping = $Xml.configuration.packageSourceMapping
# Remove invalid packageSource nodes (missing key or empty key)
$invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']")
if ($invalidNodes) {
foreach ($node in $invalidNodes) {
$mapping.RemoveChild($node) | Out-Null
}
}
# Add/Update Mapping Source
$mappingSource = $mapping.SelectSingleNode("packageSource[@key='$Key']")
if (-not $mappingSource) {
$mappingSource = $Xml.CreateElement("packageSource")
$mappingSource.SetAttribute("key", $Key)
# Insert at top for priority
if ($mapping.HasChildNodes) {
$mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null
} else {
$mapping.AppendChild($mappingSource) | Out-Null
}
}
# Double check and force attribute
if (-not $mappingSource.HasAttribute("key")) {
$mappingSource.SetAttribute("key", $Key)
}
# Update Patterns
# RemoveAll() removes all child nodes AND attributes, so we must re-set the key afterwards
$mappingSource.RemoveAll()
$mappingSource.SetAttribute("key", $Key)
foreach ($pattern in $Patterns) {
$pkg = $Xml.CreateElement("package")
$pkg.SetAttribute("pattern", $pattern)
$mappingSource.AppendChild($pkg) | Out-Null
}
}
function Resolve-WinAppSdkSplitDependencies {
Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..."
$installDir = Join-Path $rootPath "localpackages\output"
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
# Create a temporary nuget.config to avoid interference from the repo's config
$tempConfig = Join-Path $env:TEMP "nuget_$(Get-Random).config"
Set-Content -Path $tempConfig -Value "<?xml version='1.0' encoding='utf-8'?><configuration><packageSources><clear /><add key='TempSource' value='$sourceLink' /></packageSources></configuration>"
try {
# Extract BuildTools version from Directory.Packages.props to ensure we have the required version
$dirPackagesProps = Join-Path $rootPath "Directory.Packages.props"
if (Test-Path $dirPackagesProps) {
$propsContent = Get-Content $dirPackagesProps -Raw
if ($propsContent -match '<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="([^"]+)"') {
$buildToolsVersion = $Matches[1]
Write-Host "Downloading Microsoft.Windows.SDK.BuildTools version $buildToolsVersion..."
$nugetArgsBuildTools = "install Microsoft.Windows.SDK.BuildTools -Version $buildToolsVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
Invoke-Expression "nuget $nugetArgsBuildTools" | Out-Null
}
}
# Download package to inspect nuspec and keep it for the build
$nugetArgs = "install Microsoft.WindowsAppSDK -Version $WinAppSDKVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
Invoke-Expression "nuget $nugetArgs" | Out-Null
# Parse dependencies from the installed folders
# Folder structure is typically {PackageId}.{Version}
$directories = Get-ChildItem -Path $installDir -Directory
$allLocalPackages = @()
foreach ($dir in $directories) {
# Match any package pattern: PackageId.Version
if ($dir.Name -match "^(.+?)\.(\d+\..*)$") {
$pkgId = $Matches[1]
$pkgVer = $Matches[2]
$allLocalPackages += $pkgId
$packageVersions[$pkgId] = $pkgVer
Write-Host "Found dependency: $pkgId = $pkgVer"
}
}
# Update repo's nuget.config to use localpackages
$nugetConfig = Join-Path $rootPath "nuget.config"
$configData = Read-FileWithEncoding -Path $nugetConfig
[xml]$xml = $configData.Content
Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value $installDir -Patterns $allLocalPackages
$xml.Save($nugetConfig)
Write-Host "Updated nuget.config with localpackages mapping."
} catch {
Write-Warning "Failed to resolve dependencies: $_"
} finally {
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
}
}
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
# The nuget list for experimental versions will cost more time
@@ -112,56 +213,36 @@ if ($latestVersion) {
exit 1
}
# Update packages.config files
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Resolve dependencies for 1.8+
$packageVersions = @{ "Microsoft.WindowsAppSDK" = $WinAppSDKVersion }
Resolve-WinAppSdkSplitDependencies
# Update Directory.Packages.props file
Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
$content = $content -replace $oldVersionString, $newVersionString
$isModified = $false
foreach ($pkgId in $packageVersions.Keys) {
$ver = $packageVersions[$pkgId]
# Escape dots in package ID for regex
$pkgIdRegex = $pkgId -replace '\.', '\.'
$newVersionString = "<PackageVersion Include=""$pkgId"" Version=""$ver"" />"
$oldVersionString = "<PackageVersion Include=""$pkgIdRegex"" Version=""[-.0-9a-zA-Z]*"" />"
if ($content -match "<PackageVersion Include=""$pkgIdRegex""") {
# Update existing package
if ($content -notmatch [regex]::Escape($newVersionString)) {
$content = $content -replace $oldVersionString, $newVersionString
$isModified = $true
}
}
}
if ($isModified) {
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update .vcxproj files
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match '\\Microsoft.WindowsAppSDK.') {
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion
$oldVersionString = '\\Microsoft.WindowsAppSDK.(?=[-.0-9a-zA-Z]*\d)[-.0-9a-zA-Z]*' #positive lookahead for at least a digit
$content = $content -replace $oldVersionString, $newVersionString
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update .csproj files
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
Update-NugetConfig

View File

@@ -19,7 +19,7 @@ parameters:
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: true
default: false
- name: runTests
type: boolean
displayName: "Run Tests"
@@ -33,7 +33,7 @@ parameters:
default: true
- name: winAppSDKVersionNumber
type: string
default: 1.7
default: 1.8
- name: useExperimentalVersion
type: boolean
default: false

View File

@@ -129,7 +129,7 @@ jobs:
MSBuildMainBuildTargets: Build
${{ insert }}: ${{ parameters.variables }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages"'
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages" /p:IgnoreExperimentalWarnings=true'
${{ else }}:
RestoreAdditionalProjectSourcesArg: ''
displayName: Build

View File

@@ -19,48 +19,20 @@ steps:
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
-rootPath "$(build.sourcesdirectory)"
- script: echo $(WinAppSDKVersion)
displayName: 'Display WinAppSDK Version Found'
# - task: NuGetCommand@2
# displayName: 'Restore NuGet packages (slnx)'
# inputs:
# command: 'restore'
# feedsToUse: 'config'
# nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
# restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
# includeNuGetOrg: false
- task: DownloadPipelineArtifact@2
displayName: 'Download WindowsAppSDK'
inputs:
buildType: 'specific'
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
definition: '104083'
buildVersionToDownload: 'latestFromBranch'
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
targetPath: '$(Build.SourcesDirectory)\localpackages'
- script: dir $(Build.SourcesDirectory)\localpackages\NugetPackages
displayName: 'List downloaded packages'
- task: NuGetCommand@2
displayName: 'Install WindowsAppSDK'
inputs:
command: 'custom'
arguments: >
install "Microsoft.WindowsAppSDK"
-Source "$(Build.SourcesDirectory)\localpackages\NugetPackages"
-Version "$(WinAppSDKVersion)"
-OutputDirectory "$(Build.SourcesDirectory)\localpackages\output"
-FallbackSource "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
- task: NuGetCommand@2
displayName: 'Restore NuGet packages'
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet packages (dotnet)'
inputs:
command: 'restore'
projects: '$(build.sourcesdirectory)\**\*.slnx'
feedsToUse: 'config'
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
includeNuGetOrg: false
- task: NuGetCommand@2
displayName: 'Restore NuGet packages (slnx)'
inputs:
command: 'restore'
feedsToUse: 'config'
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
includeNuGetOrg: false
workingDirectory: '$(build.sourcesdirectory)'

View File

@@ -27,7 +27,8 @@ $versionExceptions = @(
"WyHash.dll",
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
"ObjectModelCsProjection.dll",
"RendererCsProjection.dll") -join '|';
"RendererCsProjection.dll",
"Microsoft.ML.OnnxRuntime.dll") -join '|';
$nullVersionExceptions = @(
"SkiaSharp.Views.WinUI.Native.dll",
"libSkiaSharp.dll",
@@ -52,7 +53,12 @@ $nullVersionExceptions = @(
"System.Diagnostics.EventLog.Messages.dll",
"Microsoft.Windows.Widgets.dll",
"AdaptiveCards.ObjectModel.WinUI3.dll",
"AdaptiveCards.Rendering.WinUI3.dll") -join '|';
"AdaptiveCards.Rendering.WinUI3.dll",
"boost_regex_vc143_mt_gd_x32_1_87.dll",
"boost_regex_vc143_mt_gd_x64_1_87.dll",
"boost_regex_vc143_mt_x32_1_87.dll",
"boost_regex_vc143_mt_x64_1_87.dll"
) -join '|';
$totalFailure = 0;
Write-Host $DirPath;

View File

@@ -121,6 +121,9 @@ PowerToys Awake is a tool to keep your computer awake.
Randy contributed Registry Preview and some very early conversations about keyboard remapping.
### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon
Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product
### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
Find My Mouse is based on Raymond Chen's SuperSonar.
@@ -180,7 +183,6 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
## PowerToys core team
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead
- [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead
@@ -209,6 +211,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
## Former PowerToys core team members
- [@indierawk2k2](https://github.com/indierawk2k2) - Mike Harsh - Product Manager
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager
- [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager
- [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager

View File

@@ -262,6 +262,7 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Command Palette
<table style="width:100%">
<tr>
<th>Event Name</th>
@@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
<td>Triggered when the Command Palette process is started.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_ExtensionInvoked</td>
<td>Tracks extension usage including extension ID, command details, success status, and execution time.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_SessionDuration</td>
<td>Logs session metrics from launch to dismissal including duration, commands executed, pages visited, search queries, navigation depth, and errors.</td>
</tr>
</table>
### Crop And Lock

View File

@@ -8,4 +8,24 @@
<PropertyGroup Label="ManifestToolOverride">
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
</PropertyGroup>
<!-- Auto-restore NuGet for native vcxproj (PackageReference) when building inside VS -->
<Target Name="EnsureNuGetRestoreForVcxproj" BeforeTargets="PrepareForBuild" Condition="
'$(BuildingInsideVisualStudio)' == 'true'
and '$(DesignTimeBuild)' != 'true'
and '$(RestoreInProgress)' != 'true'
and '$(MSBuildProjectExtension)' == '.vcxproj'
and '$(RestoreProjectStyle)' == 'PackageReference'
and '$(MSBuildProjectExtensionsPath)' != ''
and !Exists('$(MSBuildProjectExtensionsPath)project.assets.json')
">
<Message Importance="normal" Text="NuGet assets missing for $(MSBuildProjectName); running Restore...; IntDir=$(IntDir); BaseIntermediateOutputPath=$(BaseIntermediateOutputPath)" />
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Restore" Properties="RestoreInProgress=true" BuildInParallel="false" />
</Target>
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -40,6 +40,7 @@
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />

View File

@@ -976,6 +976,14 @@
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
</Folder>
<Folder Name="/settings-ui/">
<Project Path="src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -124,7 +124,7 @@
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER &gt;= 22000" />
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
<!-- TODO: Use to activate embedded MSIX -->

View File

@@ -36,5 +36,6 @@ namespace ManagedCommon
PowerOCR,
Workspaces,
ZoomIt,
GeneralSettings,
}
}

View File

@@ -16,4 +16,5 @@ public static partial class CLSID
public static readonly Guid CollatorDataSource = new Guid("9E175B8B-F52A-11D8-B9A5-505054503030");
public static readonly Guid ApplicationActivationManager = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
public static readonly Guid VirtualDesktopManager = new("aa509086-5ca9-4c25-8f95-589d3c07b48a");
public static readonly Guid DesktopWallpaper = new("C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD");
}

View File

@@ -16,6 +16,12 @@ public static partial class Ole32
CLSCTX dwClsContext,
ref Guid riid,
out IntPtr rReturnedComObject);
[LibraryImport("ole32.dll")]
internal static partial int CoInitializeEx(nint pvReserved, uint dwCoInit);
[LibraryImport("ole32.dll")]
internal static partial void CoUninitialize();
}
[Flags]

View File

@@ -119,6 +119,16 @@ namespace PowerToysSettings
class HotkeyObject
{
public:
HotkeyObject() :
m_json(json::JsonObject())
{
m_json.SetNamedValue(L"win", json::value(false));
m_json.SetNamedValue(L"ctrl", json::value(false));
m_json.SetNamedValue(L"alt", json::value(false));
m_json.SetNamedValue(L"shift", json::value(false));
m_json.SetNamedValue(L"code", json::value(0));
m_json.SetNamedValue(L"key", json::value(L""));
}
static HotkeyObject from_json(json::JsonObject json)
{
return HotkeyObject(std::move(json));

View File

@@ -0,0 +1,399 @@
// 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.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Provides methods for recording the screen during UI tests.
/// Requires FFmpeg to be installed and available in PATH.
/// </summary>
internal class ScreenRecording : IDisposable
{
private readonly string outputDirectory;
private readonly string framesDirectory;
private readonly string outputFilePath;
private readonly List<string> capturedFrames;
private readonly SemaphoreSlim recordingLock = new(1, 1);
private readonly Stopwatch recordingStopwatch = new();
private readonly string? ffmpegPath;
private CancellationTokenSource? recordingCancellation;
private Task? recordingTask;
private bool isRecording;
private int frameCount;
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
[DllImport("user32.dll")]
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
private const int CURSORSHOWING = 0x00000001;
private const int DESKTOPHORZRES = 118;
private const int DESKTOPVERTRES = 117;
private const int DINORMAL = 0x0003;
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
/// <summary>
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
/// </summary>
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
public ScreenRecording(string outputDirectory)
{
this.outputDirectory = outputDirectory;
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
capturedFrames = new List<string>();
frameCount = 0;
// Check if FFmpeg is available
ffmpegPath = FindFfmpeg();
if (ffmpegPath == null)
{
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
}
}
/// <summary>
/// Gets a value indicating whether screen recording is available (FFmpeg found).
/// </summary>
public bool IsAvailable => ffmpegPath != null;
/// <summary>
/// Starts recording the screen.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StartRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (isRecording || !IsAvailable)
{
return;
}
// Create frames directory
Directory.CreateDirectory(framesDirectory);
recordingCancellation = new CancellationTokenSource();
isRecording = true;
recordingStopwatch.Start();
// Start the recording task
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start recording: {ex.Message}");
isRecording = false;
}
finally
{
recordingLock.Release();
}
}
/// <summary>
/// Stops recording and encodes video.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StopRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (!isRecording || recordingCancellation == null)
{
return;
}
// Signal cancellation
recordingCancellation.Cancel();
// Wait for recording task to complete
if (recordingTask != null)
{
await recordingTask;
}
recordingStopwatch.Stop();
isRecording = false;
double duration = recordingStopwatch.Elapsed.TotalSeconds;
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
// Encode to video
await EncodeToVideoAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error stopping recording: {ex.Message}");
}
finally
{
Cleanup();
recordingLock.Release();
}
}
/// <summary>
/// Records frames from the screen.
/// </summary>
private void RecordFrames(CancellationToken cancellationToken)
{
try
{
int frameInterval = 1000 / TargetFps;
var frameTimer = Stopwatch.StartNew();
while (!cancellationToken.IsCancellationRequested)
{
var frameStart = frameTimer.ElapsedMilliseconds;
try
{
CaptureFrame();
}
catch (Exception ex)
{
Console.WriteLine($"Error capturing frame: {ex.Message}");
}
// Sleep for remaining time to maintain target FPS
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
if (sleepTime > 0)
{
Thread.Sleep(sleepTime);
}
}
}
catch (OperationCanceledException)
{
// Expected when stopping
}
catch (Exception ex)
{
Console.WriteLine($"Error during recording: {ex.Message}");
}
}
/// <summary>
/// Captures a single frame.
/// </summary>
private void CaptureFrame()
{
IntPtr hdc = GetDC(IntPtr.Zero);
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
ScreenCapture.CURSORINFO cursorInfo;
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
{
IntPtr hdcDest = g.GetHdc();
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
g.ReleaseHdc(hdcDest);
}
}
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
bitmap.Save(framePath, ImageFormat.Jpeg);
capturedFrames.Add(framePath);
frameCount++;
}
}
/// <summary>
/// Encodes captured frames to video using ffmpeg.
/// </summary>
private async Task EncodeToVideoAsync()
{
if (capturedFrames.Count == 0)
{
Console.WriteLine("No frames captured");
return;
}
try
{
// Build ffmpeg command with proper non-interactive flags
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
// -y: overwrite without asking
// -nostdin: disable interaction
// -loglevel error: only show errors
// -stats: show encoding progress
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath!,
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process != null)
{
// Close stdin immediately to ensure FFmpeg doesn't wait for input
process.StandardInput.Close();
// Read output streams asynchronously to prevent deadlock
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
// Wait for process to exit
await process.WaitForExitAsync();
// Get the output
string stdout = await outputTask;
string stderr = await errorTask;
if (process.ExitCode == 0 && File.Exists(outputFilePath))
{
var fileInfo = new FileInfo(outputFilePath);
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
else
{
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
if (!string.IsNullOrWhiteSpace(stderr))
{
Console.WriteLine($"FFmpeg error: {stderr}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error encoding video: {ex.Message}");
}
}
/// <summary>
/// Finds ffmpeg executable.
/// </summary>
private static string? FindFfmpeg()
{
// Check if ffmpeg is in PATH
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (var dir in pathDirs)
{
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
if (File.Exists(ffmpegPath))
{
return ffmpegPath;
}
}
// Check common installation locations
var commonPaths = new[]
{
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
@"C:\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
};
foreach (var path in commonPaths)
{
if (File.Exists(path))
{
return path;
}
}
return null;
}
/// <summary>
/// Gets the path to the recorded video file.
/// </summary>
public string OutputFilePath => outputFilePath;
/// <summary>
/// Gets the directory containing recordings.
/// </summary>
public string OutputDirectory => outputDirectory;
/// <summary>
/// Cleans up resources.
/// </summary>
private void Cleanup()
{
recordingCancellation?.Dispose();
recordingCancellation = null;
recordingTask = null;
// Clean up frames directory if it exists
try
{
if (Directory.Exists(framesDirectory))
{
Directory.Delete(framesDirectory, true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
}
}
/// <summary>
/// Disposes resources.
/// </summary>
public void Dispose()
{
if (isRecording)
{
StopRecordingAsync().GetAwaiter().GetResult();
}
Cleanup();
recordingLock.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
/// <param name="args">Optional command line arguments to pass to the application.</param>
public void StartExe(string appPath, string[]? args = null)
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
{
var opts = new AppiumOptions();
if (!string.IsNullOrEmpty(enableModules))
{
opts.AddAdditionalCapability("enableModules", enableModules);
}
if (scope == PowerToysModule.PowerToysSettings)
{
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
private void TryLaunchPowerToysSettings(AppiumOptions opts)
{
try
if (opts.ToCapabilities().HasCapability("enableModules"))
{
var runnerProcessInfo = new ProcessStartInfo
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
}
else
{
SettingsConfigHelper.ConfigureGlobalModuleSettings();
}
const int maxTries = 3;
const int delayMs = 5000;
const int maxRetries = 3;
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
{
try
{
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
var runnerProcessInfo = new ProcessStartInfo
{
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
ExitExe(runnerProcessInfo.FileName);
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
// Verify process was killed
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
var remainingProcesses = Process.GetProcessesByName(exeName);
// 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);
runner = Process.Start(runnerProcessInfo);
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
{
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
return;
}
// Window not found, kill all PowerToys processes and retry
if (tryCount < maxTries)
{
KillPowerToysProcesses();
}
}
catch (Exception ex)
{
if (tryCount == maxTries)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
}
// Kill processes and retry
KillPowerToysProcesses();
}
}
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
}
private void TryLaunchCommandPalette(AppiumOptions opts)
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
var process = Process.Start(processStartInfo);
process?.WaitForExit();
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
{
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
}
}
catch (Exception ex)
{
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
}
}
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
{
var hexHwnd = window[0].HWnd.ToString("x");
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
return;
return true;
}
if (attempt < maxRetries)
{
Thread.Sleep(delayMs);
}
else
{
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
}
}
return false;
}
/// <summary>
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
catch (Exception ex)
{
// Handle exceptions if needed
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
}
}
/// <summary>
/// Restarts now exe and takes control of it.
/// </summary>
public void RestartScopeExe()
public void RestartScopeExe(string? enableModules = null)
{
ExitScopeExe();
StartExe(locationPath + sessionPath, this.commandLineArgs);
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
}
public WindowsDriver<WindowsElement> GetRoot()
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
this.ExitExe(winAppDriverProcessInfo.FileName);
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
}
private void KillPowerToysProcesses()
{
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
foreach (var processName in powerToysProcessNames)
{
try
{
var processes = Process.GetProcessesByName(processName);
foreach (var process in processes)
{
process.Kill();
process.WaitForExit();
}
// Verify processes are actually gone
var remainingProcesses = Process.GetProcessesByName(processName);
}
catch (Exception ex)
{
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
}
}
}
}
}

View File

@@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Configures global PowerToys settings to enable only specified modules and disable all others.
/// </summary>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
{
ArgumentNullException.ThrowIfNull(modulesToEnable);
modulesToEnable ??= Array.Empty<string>();
try
{

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
public string? ScreenshotDirectory { get; set; }
public string? RecordingDirectory { get; set; }
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
private readonly PowerToysModule scope;
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
private readonly string[]? commandLineArgs;
private SessionHelper? sessionHelper;
private System.Threading.Timer? screenshotTimer;
private ScreenRecording? screenRecording;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
{
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
CloseOtherApplications();
if (IsInPipeline)
{
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(ScreenshotDirectory);
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(RecordingDirectory);
// Take screenshot every 1 second
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
// Start screen recording (requires FFmpeg)
try
{
screenRecording = new ScreenRecording(RecordingDirectory);
if (screenRecording.IsAvailable)
{
_ = screenRecording.StartRecordingAsync();
}
else
{
screenRecording = null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
screenRecording = null;
}
// Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}");
}
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
if (IsInPipeline)
{
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
Dispose();
// Stop screen recording
if (screenRecording != null)
{
try
{
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
}
}
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
or UnitTestOutcome.Error
or UnitTestOutcome.Unknown)
{
Task.Delay(1000).Wait();
AddScreenShotsToTestResultsDirectory();
AddRecordingsToTestResultsDirectory();
AddLogFilesToTestResultsDirectory();
}
else
{
// Clean up recording if test passed
CleanupRecordingDirectory();
}
Dispose();
}
this.Session.Cleanup();
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
public void Dispose()
{
screenshotTimer?.Dispose();
screenRecording?.Dispose();
GC.SuppressFinalize(this);
}
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// Adds screen recordings to test results directory when test fails.
/// </summary>
protected void AddRecordingsToTestResultsDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
// Add video files (MP4)
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
foreach (string file in videoFiles)
{
this.TestContext.AddResultFile(file);
var fileInfo = new FileInfo(file);
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
if (videoFiles.Length == 0)
{
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
}
}
}
/// <summary>
/// Cleans up recording directory when test passes.
/// </summary>
private void CleanupRecordingDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
try
{
Directory.Delete(RecordingDirectory, true);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
}
}
}
/// <summary>
/// Copies PowerToys log files to test results directory when test fails.
/// Renames files to include the directory structure after \PowerToys.
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Restart scope exe.
/// </summary>
public void RestartScopeExe()
public Session RestartScopeExe(string? enableModules = null)
{
this.sessionHelper!.RestartScopeExe();
this.sessionHelper!.RestartScopeExe(enableModules);
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
return;
return Session;
}
/// <summary>

View File

@@ -144,7 +144,7 @@ public sealed class AIServiceBatchIntegrationTests
switch (format)
{
case PasteFormats.CustomTextTransformation:
var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress);
var transformResult = await services.CustomActionTransformService.TransformAsync(batchTestInput.Prompt, batchTestInput.Clipboard, null, CancellationToken.None, progress);
return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty);
case PasteFormats.KernelQuery:

View File

@@ -198,20 +198,14 @@ namespace AdvancedPaste.Pages
}
}
private async void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
private void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
{
if (args.InvokedItem is ClipboardItem item)
if (args.InvokedItem is ClipboardItem item && item.Item is not null)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
if (!string.IsNullOrEmpty(item.Content))
{
ClipboardHelper.SetTextContent(item.Content);
}
else if (item.Image is not null)
{
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
ClipboardHelper.SetImageContent(image);
}
// Use SetHistoryItemAsContent to set the clipboard content without creating a new history entry
Clipboard.SetHistoryItemAsContent(item.Item);
}
}
}

View File

@@ -225,6 +225,24 @@ internal static class DataPackageHelpers
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
internal static async Task<byte[]> GetImageAsPngBytesAsync(this DataPackageView dataPackageView)
{
var bitmap = await dataPackageView.GetImageContentAsync();
if (bitmap == null)
{
return null;
}
using var pngStream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream);
encoder.SetSoftwareBitmap(bitmap);
await encoder.FlushAsync();
using var memoryStream = new MemoryStream();
await pngStream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
internal static async Task<SoftwareBitmap> GetImageContentAsync(this DataPackageView dataPackageView)
{
using var stream = await dataPackageView.GetImageStreamAsync();

View File

@@ -166,5 +166,8 @@ namespace AdvancedPaste.Helpers
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
[DllImport("user32.dll", SetLastError = true)]
internal static extern uint GetClipboardSequenceNumber();
}
}

View File

@@ -46,7 +46,7 @@ public enum PasteFormats
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Image,
IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText,
KernelFunctionDescription = "Takes an image in the clipboard and extracts all text from it using OCR.")]
KernelFunctionDescription = "Takes an image from the clipboard and extracts text using OCR. This function is intended only for explicit text extraction or OCR requests.")]
ImageToText,
[PasteFormatMetadata(
@@ -118,8 +118,8 @@ public enum PasteFormats
IconGlyph = "\uE945",
RequiresAIService = true,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Text,
KernelFunctionDescription = "Takes input instructions and transforms clipboard text (not TXT files) with these input instructions, putting the result back on the clipboard. This uses AI to accomplish the task.",
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Image,
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text or image). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
RequiresPrompt = true)]
CustomTextTransformation,
}

View File

@@ -40,15 +40,15 @@ namespace AdvancedPaste.Services.CustomActions
this.userSettings = userSettings;
}
public async Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
{
var pasteConfig = userSettings?.PasteAIConfiguration;
var providerConfig = BuildProviderConfig(pasteConfig);
return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
}
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
{
ArgumentNullException.ThrowIfNull(providerConfig);
@@ -57,9 +57,9 @@ namespace AdvancedPaste.Services.CustomActions
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
}
if (string.IsNullOrWhiteSpace(inputText))
if (string.IsNullOrWhiteSpace(inputText) && imageBytes is null)
{
Logger.LogWarning("Clipboard has no usable text data");
Logger.LogWarning("Clipboard has no usable data");
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
}
@@ -80,6 +80,8 @@ namespace AdvancedPaste.Services.CustomActions
{
Prompt = prompt,
InputText = inputText,
ImageBytes = imageBytes,
ImageMimeType = imageBytes != null ? "image/png" : null,
SystemPrompt = systemPrompt,
};

View File

@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
{
public interface ICustomActionTransformService
{
Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
}
}

View File

@@ -12,6 +12,10 @@ namespace AdvancedPaste.Services.CustomActions
public string InputText { get; init; }
public byte[] ImageBytes { get; init; }
public string ImageMimeType { get; init; }
public string SystemPrompt { get; init; }
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;

View File

@@ -64,21 +64,13 @@ namespace AdvancedPaste.Services.CustomActions
var prompt = request.Prompt;
var inputText = request.InputText;
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
var imageBytes = request.ImageBytes;
if (string.IsNullOrWhiteSpace(prompt) || (string.IsNullOrWhiteSpace(inputText) && imageBytes is null))
{
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
throw new ArgumentException("Prompt and input content must be provided", nameof(request));
}
var userMessageContent = $"""
User instructions:
{prompt}
Clipboard Content:
{inputText}
Output:
""";
var executionSettings = CreateExecutionSettings();
var kernel = CreateKernel();
var modelId = _config.Model;
@@ -102,7 +94,32 @@ namespace AdvancedPaste.Services.CustomActions
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage(systemPrompt);
chatHistory.AddUserMessage(userMessageContent);
if (imageBytes != null)
{
var collection = new ChatMessageContentItemCollection();
if (!string.IsNullOrWhiteSpace(inputText))
{
collection.Add(new TextContent($"Clipboard Content:\n{inputText}"));
}
collection.Add(new ImageContent(imageBytes, request.ImageMimeType ?? "image/png"));
collection.Add(new TextContent($"User instructions:\n{prompt}\n\nOutput:"));
chatHistory.AddUserMessage(collection);
}
else
{
var userMessageContent = $"""
User instructions:
{prompt}
Clipboard Content:
{inputText}
Output:
""";
chatHistory.AddUserMessage(userMessageContent);
}
var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
chatHistory.Add(response);

View File

@@ -67,12 +67,36 @@ public abstract class KernelServiceBase(
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
var outputPackage = kernel.GetDataPackage();
var hasUsableData = await outputPackage.GetView().HasUsableDataAsync();
if (kernel.GetLastError() is Exception ex)
{
throw ex;
// If we have an error, but the AI provided a final text response, we can ignore the error (likely a tool failure that the AI handled).
// However, if we have usable data (e.g. from a successful tool call before the error?), we might want to keep it?
// In the case of ImageToText failure, outputPackage is empty (new DataPackage), hasUsableData is false.
// So we check if there is a valid response in the chat history.
var lastMessage = chatHistory.LastOrDefault();
bool hasAssistantResponse = lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content);
if (!hasAssistantResponse && !hasUsableData)
{
throw ex;
}
// If we have a response or data, we log the error but proceed.
Logger.LogWarning($"Kernel operation encountered an error but proceeded with available response/data: {ex.Message}");
}
var outputPackage = kernel.GetDataPackage();
if (!hasUsableData)
{
var lastMessage = chatHistory.LastOrDefault();
if (lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content))
{
outputPackage = DataPackageHelpers.CreateFromText(lastMessage.Content);
kernel.SetDataPackage(outputPackage);
}
}
if (!(await outputPackage.GetView().HasUsableDataAsync()))
{
@@ -148,7 +172,21 @@ public abstract class KernelServiceBase(
var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt;
chatHistory.AddSystemMessage(systemPrompt);
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
chatHistory.AddUserMessage(prompt);
var imageBytes = await kernel.GetDataPackageView().GetImageAsPngBytesAsync();
if (imageBytes != null)
{
var collection = new ChatMessageContentItemCollection
{
new TextContent(prompt),
new ImageContent(imageBytes, "image/png"),
};
chatHistory.AddUserMessage(collection);
}
else
{
chatHistory.AddUserMessage(prompt);
}
if (ShouldModerateAdvancedAI())
{
@@ -302,8 +340,16 @@ public abstract class KernelServiceBase(
new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
async dataPackageView =>
{
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
var input = await dataPackageView.GetTextOrHtmlTextAsync();
if (string.IsNullOrEmpty(input) && imageBytes == null)
{
// If we have no text and no image, try to get text via OCR or throw if nothing exists
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
}
var result = await _customActionTransformService.TransformAsync(fixedPrompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
});
@@ -313,15 +359,22 @@ public abstract class KernelServiceBase(
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
async dataPackageView =>
{
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
var input = await dataPackageView.GetTextOrHtmlTextAsync();
if (string.IsNullOrEmpty(input) && imageBytes == null)
{
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
}
string output = await GetPromptBasedOutput(format, prompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(output);
});
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress) =>
format switch
{
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformAsync(prompt, input, imageBytes, cancellationToken, progress))?.Content ?? string.Empty,
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
};

View File

@@ -37,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
pasteFormat.Format switch
{
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
});
}

View File

@@ -45,6 +45,7 @@ namespace AdvancedPaste.ViewModels
private CancellationTokenSource _pasteActionCancellationTokenSource;
private string _currentClipboardHistoryId;
private uint _lastClipboardSequenceNumber;
private DateTimeOffset? _currentClipboardTimestamp;
private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None;
private bool _clipboardHistoryUnavailableLogged;
@@ -455,6 +456,7 @@ namespace AdvancedPaste.ViewModels
{
ResetClipboardPreview();
_currentClipboardHistoryId = null;
_lastClipboardSequenceNumber = 0;
_currentClipboardTimestamp = null;
_lastClipboardFormats = ClipboardFormat.None;
return;
@@ -477,6 +479,13 @@ namespace AdvancedPaste.ViewModels
{
bool clipboardChanged = formatsChanged;
var currentSequenceNumber = NativeMethods.GetClipboardSequenceNumber();
if (_lastClipboardSequenceNumber != currentSequenceNumber)
{
clipboardChanged = true;
_lastClipboardSequenceNumber = currentSequenceNumber;
}
if (Clipboard.IsHistoryEnabled())
{
try

View File

@@ -312,13 +312,39 @@ private:
return false;
}
void read_settings(PowerToysSettings::PowerToyValues& settings)
void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
// Migrate Paste As Plain text shortcut
Hotkey old_paste_as_plain_hotkey;
bool old_data_migrated = migrate_data_and_remove_data_file(old_paste_as_plain_hotkey);
if (settingsObject.GetView().Size())
{
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
{
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
}
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
{
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
}
else
{
m_is_ai_enabled = false;
}
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
{
m_preview_custom_format_output = propertiesObject.GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
}
}
if (old_data_migrated)
{
m_paste_as_plain_hotkey = old_paste_as_plain_hotkey;
@@ -405,31 +431,6 @@ private:
}
}
}
if (settingsObject.GetView().Size())
{
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
{
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
}
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
{
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
}
else
{
m_is_ai_enabled = false;
}
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
{
m_preview_custom_format_output = propertiesObject.GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
}
}
}
// Load the settings file.

View File

@@ -50,6 +50,7 @@ enum class ScheduleMode
Off,
FixedHours,
SunsetToSunrise,
FollowNightLight,
// add more later
};
@@ -61,6 +62,8 @@ inline std::wstring ToString(ScheduleMode mode)
return L"SunsetToSunrise";
case ScheduleMode::FixedHours:
return L"FixedHours";
case ScheduleMode::FollowNightLight:
return L"FollowNightLight";
default:
return L"Off";
}
@@ -72,6 +75,8 @@ inline ScheduleMode FromString(const std::wstring& str)
return ScheduleMode::SunsetToSunrise;
if (str == L"FixedHours")
return ScheduleMode::FixedHours;
if (str == L"FollowNightLight")
return ScheduleMode::FollowNightLight;
return ScheduleMode::Off;
}
@@ -167,7 +172,9 @@ public:
ToString(g_settings.m_scheduleMode),
{ { L"Off", L"Disable the schedule" },
{ L"FixedHours", L"Set hours manually" },
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
{ L"SunsetToSunrise", L"Use sunrise/sunset times" },
{ L"FollowNightLight", L"Follow Windows Night Light state" }
});
// Integer spinners
settings.add_int_spinner(

View File

@@ -13,10 +13,12 @@
#include <utils/logger_helper.h>
#include "LightSwitchStateManager.h"
#include <LightSwitchUtils.h>
#include <NightLightRegistryObserver.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
HANDLE g_ServiceStopEvent = nullptr;
static LightSwitchStateManager* g_stateManagerPtr = nullptr;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
@@ -168,7 +170,15 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan
}
// Use shared helper (handles wraparound logic)
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
bool shouldBeLight = false;
if (s.scheduleMode == ScheduleMode::FollowNightLight)
{
shouldBeLight = !IsNightLightEnabled();
}
else
{
shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
}
// Compare current system/apps theme
bool currentSystemLight = GetCurrentSystemTheme();
@@ -199,15 +209,40 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
// Initialization
// ────────────────────────────────────────────────────────────────
static LightSwitchStateManager stateManager;
g_stateManagerPtr = &stateManager;
LightSwitchSettings::instance().InitFileWatcher();
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
static std::unique_ptr<NightLightRegistryObserver> g_nightLightWatcher;
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
// after loading settings:
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
if (nightLightNeeded && !g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
HKEY_CURRENT_USER,
NIGHT_LIGHT_REGISTRY_PATH,
[]() {
if (g_stateManagerPtr)
g_stateManagerPtr->OnNightLightChange();
});
}
else if (!nightLightNeeded && g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
g_nightLightWatcher->Stop();
g_nightLightWatcher.reset();
}
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
@@ -274,6 +309,31 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
ResetEvent(hSettingsChanged);
LightSwitchSettings::instance().LoadSettings();
stateManager.OnSettingsChanged();
const auto& settings = LightSwitchSettings::instance().settings();
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
if (nightLightNeeded && !g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
HKEY_CURRENT_USER,
NIGHT_LIGHT_REGISTRY_PATH,
[]() {
if (g_stateManagerPtr)
g_stateManagerPtr->OnNightLightChange();
});
stateManager.OnNightLightChange();
}
else if (!nightLightNeeded && g_nightLightWatcher)
{
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
g_nightLightWatcher->Stop();
g_nightLightWatcher.reset();
}
continue;
}
}
@@ -285,6 +345,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
CloseHandle(hManualOverride);
if (hParent)
CloseHandle(hParent);
if (g_nightLightWatcher)
{
g_nightLightWatcher->Stop();
g_nightLightWatcher.reset();
}
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
return 0;

View File

@@ -76,6 +76,7 @@
<ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="LightSwitchStateManager.cpp" />
<ClCompile Include="NightLightRegistryObserver.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" />
@@ -88,6 +89,7 @@
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="LightSwitchStateManager.h" />
<ClInclude Include="LightSwitchUtils.h" />
<ClInclude Include="NightLightRegistryObserver.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />

View File

@@ -36,6 +36,9 @@
<ClCompile Include="LightSwitchStateManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="NightLightRegistryObserver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
@@ -62,6 +65,9 @@
<ClInclude Include="LightSwitchUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="NightLightRegistryObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -19,7 +19,8 @@ enum class ScheduleMode
{
Off,
FixedHours,
SunsetToSunrise
SunsetToSunrise,
FollowNightLight,
// Add more in the future
};
@@ -31,6 +32,8 @@ inline std::wstring ToString(ScheduleMode mode)
return L"FixedHours";
case ScheduleMode::SunsetToSunrise:
return L"SunsetToSunrise";
case ScheduleMode::FollowNightLight:
return L"FollowNightLight";
default:
return L"Off";
}
@@ -42,6 +45,8 @@ inline ScheduleMode FromString(const std::wstring& str)
return ScheduleMode::SunsetToSunrise;
if (str == L"FixedHours")
return ScheduleMode::FixedHours;
if (str == L"FollowNightLight")
return ScheduleMode::FollowNightLight;
else
return ScheduleMode::Off;
}

View File

@@ -31,7 +31,10 @@ void LightSwitchStateManager::OnSettingsChanged()
void LightSwitchStateManager::OnTick(int currentMinutes)
{
std::lock_guard<std::mutex> lock(_stateMutex);
EvaluateAndApplyIfNeeded();
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
{
EvaluateAndApplyIfNeeded();
}
}
// Called when manual override is triggered
@@ -49,8 +52,38 @@ void LightSwitchStateManager::OnManualOverride()
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
}
EvaluateAndApplyIfNeeded();
}
// Runs with the registry observer detects a change in Night Light settings.
void LightSwitchStateManager::OnNightLightChange()
{
std::lock_guard<std::mutex> lock(_stateMutex);
bool newNightLightState = IsNightLightEnabled();
// In Follow Night Light mode, treat a Night Light toggle as a boundary
if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride)
{
Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; "
L"treating as a boundary and clearing manual override.");
_state.isManualOverride = false;
}
if (newNightLightState != _state.isNightLightActive)
{
Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}",
newNightLightState ? L"ON" : L"OFF");
_state.isNightLightActive = newNightLightState;
}
else
{
Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change.");
}
EvaluateAndApplyIfNeeded();
@@ -77,9 +110,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
_state.isSystemLightActive ? L"light" : L"dark");
_state.isSystemLightActive ? L"light" : L"dark");
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
_state.isAppsLightActive ? L"light" : L"dark");
_state.isAppsLightActive ? L"light" : L"dark");
}
static std::pair<int, int> update_sun_times(auto& settings)
@@ -194,7 +227,15 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.lastAppliedMode = _currentSettings.scheduleMode;
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
bool shouldBeLight = false;
if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight)
{
shouldBeLight = !_state.isNightLightActive;
}
else
{
shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
}
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
@@ -227,6 +268,3 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.lastTickMinutes = now;
}

View File

@@ -9,6 +9,7 @@ struct LightSwitchState
bool isManualOverride = false;
bool isSystemLightActive = false;
bool isAppsLightActive = false;
bool isNightLightActive = false;
int lastEvaluatedDay = -1;
int lastTickMinutes = -1;
@@ -32,6 +33,9 @@ public:
// Called when manual override is toggled (via shortcut or system change).
void OnManualOverride();
// Called when night light changes in windows settings
void OnNightLightChange();
// Initial sync at startup to align internal state with system theme
void SyncInitialThemeState();

View File

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

View File

@@ -0,0 +1,134 @@
#pragma once
#include <wtypes.h>
#include <string>
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
class NightLightRegistryObserver
{
public:
NightLightRegistryObserver(HKEY root, const std::wstring& subkey, std::function<void()> callback) :
_root(root), _subkey(subkey), _callback(std::move(callback)), _stop(false)
{
_thread = std::thread([this]() { this->Run(); });
}
~NightLightRegistryObserver()
{
Stop();
}
void Stop()
{
_stop = true;
{
std::lock_guard<std::mutex> lock(_mutex);
if (_event)
SetEvent(_event);
}
if (_thread.joinable())
_thread.join();
std::lock_guard<std::mutex> lock(_mutex);
if (_hKey)
{
RegCloseKey(_hKey);
_hKey = nullptr;
}
if (_event)
{
CloseHandle(_event);
_event = nullptr;
}
}
private:
void Run()
{
{
std::lock_guard<std::mutex> lock(_mutex);
if (RegOpenKeyExW(_root, _subkey.c_str(), 0, KEY_NOTIFY, &_hKey) != ERROR_SUCCESS)
return;
_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!_event)
{
RegCloseKey(_hKey);
_hKey = nullptr;
return;
}
}
while (!_stop)
{
HKEY hKeyLocal = nullptr;
HANDLE eventLocal = nullptr;
{
std::lock_guard<std::mutex> lock(_mutex);
if (_stop)
break;
hKeyLocal = _hKey;
eventLocal = _event;
}
if (!hKeyLocal || !eventLocal)
break;
if (_stop)
break;
if (RegNotifyChangeKeyValue(hKeyLocal, FALSE, REG_NOTIFY_CHANGE_LAST_SET, eventLocal, TRUE) != ERROR_SUCCESS)
break;
DWORD wait = WaitForSingleObject(eventLocal, INFINITE);
if (_stop || wait == WAIT_FAILED)
break;
ResetEvent(eventLocal);
if (!_stop && _callback)
{
try
{
_callback();
}
catch (...)
{
}
}
}
{
std::lock_guard<std::mutex> lock(_mutex);
if (_hKey)
{
RegCloseKey(_hKey);
_hKey = nullptr;
}
if (_event)
{
CloseHandle(_event);
_event = nullptr;
}
}
}
HKEY _root;
std::wstring _subkey;
std::function<void()> _callback;
HANDLE _event = nullptr;
HKEY _hKey = nullptr;
std::thread _thread;
std::atomic<bool> _stop;
std::mutex _mutex;
};

View File

@@ -11,4 +11,7 @@ enum class SettingId
Sunset_Offset,
ChangeSystem,
ChangeApps
};
};
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";

View File

@@ -3,6 +3,7 @@
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include "ThemeHelper.h"
#include <SettingsConstants.h>
// Controls changing the themes.
@@ -10,7 +11,7 @@ static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
@@ -31,7 +32,7 @@ void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
@@ -50,7 +51,7 @@ void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
@@ -79,7 +80,7 @@ bool GetCurrentSystemTheme()
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
@@ -98,7 +99,7 @@ bool GetCurrentAppsTheme()
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
@@ -109,3 +110,30 @@ bool GetCurrentAppsTheme()
return value == 1; // true = light, false = dark
}
bool IsNightLightEnabled()
{
HKEY hKey;
const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH;
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return false;
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
DWORD size = 0;
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
{
RegCloseKey(hKey);
return false;
}
std::vector<BYTE> data(size);
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
{
RegCloseKey(hKey);
return false;
}
RegCloseKey(hKey);
return data[23] == 0x10 && data[24] == 0x00;
}

View File

@@ -3,3 +3,4 @@ void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();
bool IsNightLightEnabled();

View File

@@ -617,6 +617,8 @@ namespace MouseUtils.UITests
private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false)
{
Session = RestartScopeExe("FindMyMouse,MouseHighlighter,MouseJump,MousePointerCrosshairs,CursorWrap");
// this.Session.Attach(PowerToysModule.PowerToysSettings);
this.Session.SetMainWindowSize(WindowSize.Large);

View File

@@ -8,6 +8,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -16,6 +17,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{
public ExtensionObject<ICommandItem> Model => _commandItemModel;
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
@@ -65,6 +68,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
public DataPackageView? DataPackage { get; private set; }
public List<IContextItemViewModel> AllCommands
{
get
@@ -157,6 +162,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// will never be able to load Hotkeys & aliases
UpdateProperty(nameof(IsInitialized));
if (model is IExtendedAttributesProvider extendedAttributesProvider)
{
ExtendedAttributesProvider = new ExtensionObject<IExtendedAttributesProvider>(extendedAttributesProvider);
var properties = extendedAttributesProvider.GetProperties();
UpdateDataPackage(properties);
}
Initialized |= InitializedState.Initialized;
}
@@ -379,6 +391,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands));
break;
case nameof(DataPackage):
UpdateDataPackage(ExtendedAttributesProvider?.Unsafe?.GetProperties());
break;
}
@@ -431,6 +446,16 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(Icon));
}
private void UpdateDataPackage(IDictionary<string, object?>? properties)
{
DataPackage =
properties?.TryGetValue(WellKnownExtensionAttributes.DataPackage, out var dataPackageView) == true &&
dataPackageView is DataPackageView view
? view
: null;
UpdateProperty(nameof(DataPackage));
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();

View File

@@ -19,6 +19,8 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
public string Body { get; private set; } = string.Empty;
public ContentSize? Size { get; private set; } = ContentSize.Small;
// Metadata is an array of IDetailsElement,
// where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator}
public List<DetailsElementViewModel> Metadata { get; private set; } = [];
@@ -40,6 +42,21 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
UpdateProperty(nameof(Body));
UpdateProperty(nameof(HeroImage));
if (model is IExtendedAttributesProvider provider)
{
if (provider.GetProperties()?.TryGetValue("Size", out var rawValue) == true)
{
if (rawValue is int sizeAsInt)
{
Size = (ContentSize)sizeAsInt;
}
}
}
Size ??= ContentSize.Small;
UpdateProperty(nameof(Size));
var meta = model.Metadata;
if (meta is not null)
{

View File

@@ -5,6 +5,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -57,7 +58,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
// because each call to GetProperties() is a cross process hop, and if you
// marshal-by-value the property set, then you don't want to throw it away and
// re-marshal it for every property. MAKE SURE YOU CACHE IT.
if (props?.TryGetValue("FontFamily", out var family) ?? false)
if (props?.TryGetValue(WellKnownExtensionAttributes.FontFamily, out var family) ?? false)
{
FontFamily = family as string;
}

View File

@@ -24,6 +24,8 @@ public partial class ListItemViewModel : CommandItemViewModel
public string Section { get; private set; } = string.Empty;
public bool IsSectionOrSeparator { get; private set; }
public DetailsViewModel? Details { get; private set; }
[MemberNotNullWhen(true, nameof(Details))]
@@ -82,14 +84,18 @@ public partial class ListItemViewModel : CommandItemViewModel
}
UpdateTags(li.Tags);
Section = li.Section ?? string.Empty;
UpdateProperty(nameof(Section));
IsSectionOrSeparator = IsSeparator(li);
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
UpdateAccessibleName();
}
private bool IsSeparator(IListItem item)
{
return item.Command is null;
}
public override void SlowInitializeProperties()
{
base.SlowInitializeProperties();
@@ -104,8 +110,7 @@ public partial class ListItemViewModel : CommandItemViewModel
{
Details = new(extensionDetails, PageContext);
Details.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
UpdateProperty(nameof(Details), nameof(HasDetails));
}
AddShowDetailsCommands();
@@ -135,14 +140,18 @@ public partial class ListItemViewModel : CommandItemViewModel
break;
case nameof(model.Section):
Section = model.Section ?? string.Empty;
UpdateProperty(nameof(Section));
IsSectionOrSeparator = IsSeparator(model);
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
break;
case nameof(model.Details):
case nameof(model.Command):
IsSectionOrSeparator = IsSeparator(model);
UpdateProperty(nameof(IsSectionOrSeparator));
break;
case nameof(Details):
var extensionDetails = model.Details;
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties();
UpdateProperty(nameof(Details));
UpdateProperty(nameof(HasDetails));
UpdateProperty(nameof(Details), nameof(HasDetails));
UpdateShowDetailsCommand();
break;
case nameof(model.MoreCommands):
@@ -194,8 +203,7 @@ public partial class ListItemViewModel : CommandItemViewModel
MoreCommands.Add(showDetailsContextItemViewModel);
}
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
}
}
@@ -227,8 +235,7 @@ public partial class ListItemViewModel : CommandItemViewModel
showDetailsContextItemViewModel.SlowInitializeProperties();
MoreCommands.Add(showDetailsContextItemViewModel);
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Message sent when an error occurs during command execution.
/// Used to track session error count for telemetry.
/// </summary>
public record ErrorOccurredMessage();

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Message sent when an extension command or page is invoked.
/// Captures extension usage metrics for telemetry tracking.
/// </summary>
public record ExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs);

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Message containing the current navigation depth (BackStack count) when navigating to a page.
/// Used to track maximum navigation depth reached during a session for telemetry.
/// </summary>
public record NavigationDepthMessage(int Depth);

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Message sent when a search query is executed in the Command Palette.
/// Used to track session search activity for telemetry.
/// </summary>
public record SearchQueryMessage();

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Message containing session telemetry data from Command Palette launch to dismissal.
/// Used to aggregate metrics like duration, commands executed, pages visited, and search activity.
/// </summary>
public record SessionDurationMessage(ulong DurationMs, int CommandsExecuted, int PagesVisited, string DismissalReason, int SearchQueriesCount, int MaxNavigationDepth, int ErrorCount);

View File

@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Telemetry message sent when command invocation begins.
/// </summary>
public record TelemetryBeginInvokeMessage;

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Telemetry message sent when an extension command or page is invoked.
/// Captures extension usage metrics for telemetry tracking.
/// </summary>
public record TelemetryExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs);

View File

@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Telemetry message sent when command invocation completes with a result.
/// </summary>
public record TelemetryInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind);

View File

@@ -3,13 +3,14 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class SeparatorViewModel() :
CommandItem,
IContextItemViewModel,
IFilterItemViewModel,
ISeparatorContextItem,

View File

@@ -14,6 +14,7 @@ using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ShellViewModel : ObservableObject,
IDisposable,
IRecipient<PerformCommandMessage>,
IRecipient<HandleCommandResultMessage>
{
@@ -269,8 +270,18 @@ public partial class ShellViewModel : ObservableObject,
var isMainPage = command == _rootPage;
_isNested = !isMainPage;
// Telemetry: Track extension page navigation for session metrics
if (host is not null)
{
string extensionId = host.GetExtensionDisplayName() ?? "builtin";
string commandId = command?.Id ?? "unknown";
string commandName = command?.Name ?? "unknown";
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
new(extensionId, commandId, commandName, true, 0));
}
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!);
if (pageViewModel is null)
{
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
@@ -305,7 +316,7 @@ public partial class ShellViewModel : ObservableObject,
{
CoreLogger.LogDebug($"Invoking command");
WeakReferenceMessenger.Default.Send<BeginInvokeMessage>();
WeakReferenceMessenger.Default.Send<TelemetryBeginInvokeMessage>();
StartInvoke(message, invokable, host);
}
}
@@ -338,6 +349,14 @@ public partial class ShellViewModel : ObservableObject,
private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host)
{
// Telemetry: Track command execution time and success
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var command = message.Command.Unsafe;
string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
string commandId = command?.Id ?? "unknown";
string commandName = command?.Name ?? "unknown";
bool success = false;
try
{
// Call out to extension process.
@@ -348,16 +367,28 @@ public partial class ShellViewModel : ObservableObject,
// But if it did succeed, we need to handle the result.
UnsafeHandleCommandResult(result);
success = true;
_handleInvokeTask = null;
}
catch (Exception ex)
{
success = false;
_handleInvokeTask = null;
// Telemetry: Track errors for session metrics
WeakReferenceMessenger.Default.Send<ErrorOccurredMessage>(new());
// TODO: It would be better to do this as a page exception, rather
// than a silent log message.
host?.Log(ex.Message);
}
finally
{
// Telemetry: Send extension invocation metrics (always sent, even on failure)
stopwatch.Stop();
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
new(extensionId, commandId, commandName, success, (ulong)stopwatch.ElapsedMilliseconds));
}
}
private void UnsafeHandleCommandResult(ICommandResult? result)
@@ -371,7 +402,7 @@ public partial class ShellViewModel : ObservableObject,
var kind = result.Kind;
CoreLogger.LogDebug($"handling {kind.ToString()}");
WeakReferenceMessenger.Default.Send<CmdPalInvokeResultMessage>(new(kind));
WeakReferenceMessenger.Default.Send<TelemetryInvokeResultMessage>(new(kind));
switch (kind)
{
case CommandResultKind.Dismiss:
@@ -460,4 +491,12 @@ public partial class ShellViewModel : ObservableObject,
{
_navigationCts?.Cancel();
}
public void Dispose()
{
_handleInvokeTask?.Dispose();
_navigationCts?.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,390 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
using Windows.UI.ViewManagement;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
{
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
// row 0
Color.FromArgb(255, 255, 185, 0), // #ffb900
Color.FromArgb(255, 255, 140, 0), // #ff8c00
Color.FromArgb(255, 247, 99, 12), // #f7630c
Color.FromArgb(255, 202, 80, 16), // #ca5010
Color.FromArgb(255, 218, 59, 1), // #da3b01
Color.FromArgb(255, 239, 105, 80), // #ef6950
// row 1
Color.FromArgb(255, 209, 52, 56), // #d13438
Color.FromArgb(255, 255, 67, 67), // #ff4343
Color.FromArgb(255, 231, 72, 86), // #e74856
Color.FromArgb(255, 232, 17, 35), // #e81123
Color.FromArgb(255, 234, 0, 94), // #ea005e
Color.FromArgb(255, 195, 0, 82), // #c30052
// row 2
Color.FromArgb(255, 227, 0, 140), // #e3008c
Color.FromArgb(255, 191, 0, 119), // #bf0077
Color.FromArgb(255, 194, 57, 179), // #c239b3
Color.FromArgb(255, 154, 0, 137), // #9a0089
Color.FromArgb(255, 0, 120, 212), // #0078d4
Color.FromArgb(255, 0, 99, 177), // #0063b1
// row 3
Color.FromArgb(255, 142, 140, 216), // #8e8cd8
Color.FromArgb(255, 107, 105, 214), // #6b69d6
Color.FromArgb(255, 135, 100, 184), // #8764b8
Color.FromArgb(255, 116, 77, 169), // #744da9
Color.FromArgb(255, 177, 70, 194), // #b146c2
Color.FromArgb(255, 136, 23, 152), // #881798
// row 4
Color.FromArgb(255, 0, 153, 188), // #0099bc
Color.FromArgb(255, 45, 125, 154), // #2d7d9a
Color.FromArgb(255, 0, 183, 195), // #00b7c3
Color.FromArgb(255, 3, 131, 135), // #038387
Color.FromArgb(255, 0, 178, 148), // #00b294
Color.FromArgb(255, 1, 133, 116), // #018574
// row 5
Color.FromArgb(255, 0, 204, 106), // #00cc6a
Color.FromArgb(255, 16, 137, 62), // #10893e
Color.FromArgb(255, 122, 117, 116), // #7a7574
Color.FromArgb(255, 93, 90, 88), // #5d5a58
Color.FromArgb(255, 104, 118, 138), // #68768a
Color.FromArgb(255, 81, 92, 107), // #515c6b
// row 6
Color.FromArgb(255, 86, 124, 115), // #567c73
Color.FromArgb(255, 72, 104, 96), // #486860
Color.FromArgb(255, 73, 130, 5), // #498205
Color.FromArgb(255, 16, 124, 16), // #107c10
Color.FromArgb(255, 118, 118, 118), // #767676
Color.FromArgb(255, 76, 74, 72), // #4c4a48
// row 7
Color.FromArgb(255, 105, 121, 126), // #69797e
Color.FromArgb(255, 74, 84, 89), // #4a5459
Color.FromArgb(255, 100, 124, 100), // #647c64
Color.FromArgb(255, 82, 94, 84), // #525e54
Color.FromArgb(255, 132, 117, 69), // #847545
Color.FromArgb(255, 126, 115, 95), // #7e735f
];
private readonly SettingsModel _settings;
private readonly UISettings _uiSettings;
private readonly IThemeService _themeService;
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
private ElementTheme? _elementThemeOverride;
private Color _currentSystemAccentColor;
public ObservableCollection<Color> Swatches => WindowsColorSwatches;
public int ThemeIndex
{
get => (int)_settings.Theme;
set => Theme = (UserTheme)value;
}
public UserTheme Theme
{
get => _settings.Theme;
set
{
if (_settings.Theme != value)
{
_settings.Theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ThemeIndex));
Save();
}
}
}
public ColorizationMode ColorizationMode
{
get => _settings.ColorizationMode;
set
{
if (_settings.ColorizationMode != value)
{
_settings.ColorizationMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
OnPropertyChanged(nameof(IsNoBackgroundVisible));
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
if (value == ColorizationMode.WindowsAccentColor)
{
ThemeColor = _currentSystemAccentColor;
}
IsColorizationDetailsExpanded = value != ColorizationMode.None;
Save();
}
}
}
public int ColorizationModeIndex
{
get => (int)_settings.ColorizationMode;
set => ColorizationMode = (ColorizationMode)value;
}
public Color ThemeColor
{
get => _settings.CustomThemeColor;
set
{
if (_settings.CustomThemeColor != value)
{
_settings.CustomThemeColor = value;
OnPropertyChanged();
if (ColorIntensity == 0)
{
ColorIntensity = 100;
}
Save();
}
}
}
public int ColorIntensity
{
get => _settings.CustomThemeColorIntensity;
set
{
_settings.CustomThemeColorIntensity = value;
OnPropertyChanged();
Save();
}
}
public string BackgroundImagePath
{
get => _settings.BackgroundImagePath ?? string.Empty;
set
{
if (_settings.BackgroundImagePath != value)
{
_settings.BackgroundImagePath = value;
OnPropertyChanged();
if (BackgroundImageOpacity == 0)
{
BackgroundImageOpacity = 100;
}
Save();
}
}
}
public int BackgroundImageOpacity
{
get => _settings.BackgroundImageOpacity;
set
{
if (_settings.BackgroundImageOpacity != value)
{
_settings.BackgroundImageOpacity = value;
OnPropertyChanged();
Save();
}
}
}
public int BackgroundImageBrightness
{
get => _settings.BackgroundImageBrightness;
set
{
if (_settings.BackgroundImageBrightness != value)
{
_settings.BackgroundImageBrightness = value;
OnPropertyChanged();
Save();
}
}
}
public int BackgroundImageBlurAmount
{
get => _settings.BackgroundImageBlurAmount;
set
{
if (_settings.BackgroundImageBlurAmount != value)
{
_settings.BackgroundImageBlurAmount = value;
OnPropertyChanged();
Save();
}
}
}
public BackgroundImageFit BackgroundImageFit
{
get => _settings.BackgroundImageFit;
set
{
if (_settings.BackgroundImageFit != value)
{
_settings.BackgroundImageFit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageFitIndex));
Save();
}
}
}
public int BackgroundImageFitIndex
{
// Naming between UI facing string and enum is a bit confusing, but the enum fields
// are based on XAML Stretch enum values. So I'm choosing to keep the confusion here, close
// to the UI.
// - BackgroundImageFit.Fill corresponds to "Stretch"
// - BackgroundImageFit.UniformToFill corresponds to "Fill"
get => BackgroundImageFit switch
{
BackgroundImageFit.Fill => 1,
_ => 0,
};
set => BackgroundImageFit = value switch
{
1 => BackgroundImageFit.Fill,
_ => BackgroundImageFit.UniformToFill,
};
}
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
public Color EffectiveThemeColor => ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
_ => Colors.Transparent,
};
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
public ImageSource? EffectiveBackgroundImageSource =>
ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_settings = settings;
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
UpdateAccentColor(_uiSettings);
Reapply();
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
private void UpdateAccentColor(UISettings sender)
{
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
{
ThemeColor = _currentSystemAccentColor;
}
}
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
{
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
private void Save()
{
SettingsModel.SaveSettings(_settings);
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
private void Reapply()
{
// Theme services recalculates effective color and opacity based on current settings.
EffectiveBackdrop = _themeService.Current.BackdropParameters;
OnPropertyChanged(nameof(EffectiveBackdrop));
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
OnPropertyChanged(nameof(EffectiveThemeColor));
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
// LOAD BEARING:
// We need to cycle through the EffectiveTheme property to force reload of resources.
_elementThemeOverride = ElementTheme.Light;
OnPropertyChanged(nameof(EffectiveTheme));
_elementThemeOverride = ElementTheme.Dark;
OnPropertyChanged(nameof(EffectiveTheme));
_elementThemeOverride = null;
OnPropertyChanged(nameof(EffectiveTheme));
}
[RelayCommand]
private void ResetBackgroundImageProperties()
{
BackgroundImageBrightness = 0;
BackgroundImageBlurAmount = 0;
BackgroundImageFit = BackgroundImageFit.UniformToFill;
BackgroundImageOpacity = 100;
ColorIntensity = 0;
}
public void Dispose()
{
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
public enum BackgroundImageFit
{
Fill,
UniformToFill,
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
public enum ColorizationMode
{
None,
WindowsAccentColor,
CustomColor,
Image,
}

View File

@@ -34,6 +34,6 @@ public sealed partial class CommandPaletteHost : AppExtensionHost, IExtensionHos
public override string? GetExtensionDisplayName()
{
return Extension?.ExtensionDisplayName;
return Extension?.ExtensionDisplayName ?? _builtInProvider?.DisplayName ?? _builtInProvider?.Id;
}
}

View File

@@ -0,0 +1,70 @@
// 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 CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class MainWindowViewModel : ObservableObject, IDisposable
{
private readonly IThemeService _themeService;
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
[ObservableProperty]
public partial ImageSource? BackgroundImageSource { get; private set; }
[ObservableProperty]
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
[ObservableProperty]
public partial double BackgroundImageOpacity { get; private set; }
[ObservableProperty]
public partial Color BackgroundImageTint { get; private set; }
[ObservableProperty]
public partial double BackgroundImageTintIntensity { get; private set; }
[ObservableProperty]
public partial int BackgroundImageBlurAmount { get; private set; }
[ObservableProperty]
public partial double BackgroundImageBrightness { get; private set; }
[ObservableProperty]
public partial bool ShowBackgroundImage { get; private set; }
public MainWindowViewModel(IThemeService themeService)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeService_ThemeChanged;
}
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
{
_uiDispatcherQueue.TryEnqueue(() =>
{
BackgroundImageSource = _themeService.Current.BackgroundImageSource;
BackgroundImageStretch = _themeService.Current.BackgroundImageStretch;
BackgroundImageOpacity = _themeService.Current.BackgroundImageOpacity;
BackgroundImageBrightness = _themeService.Current.BackgroundBrightness;
BackgroundImageTint = _themeService.Current.Tint;
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
ShowBackgroundImage = BackgroundImageSource != null;
});
}
public void Dispose()
{
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
GC.SuppressFinalize(this);
}
}

View File

@@ -23,11 +23,12 @@
<PackageReference Include="CommunityToolkit.Common" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="AdaptiveCards.Templating" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="Microsoft.Bot.AdaptiveExpressions.Core" />
<PackageReference Include="AdaptiveCards.ObjectModel.WinUI3" GeneratePathProperty="true">
<ExcludeAssets>compile</ExcludeAssets>
</PackageReference>
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True" >
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True">
<ExcludeAssets>compile</ExcludeAssets>
</PackageReference>

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -411,6 +411,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Pick background image.
/// </summary>
public static string builtin_settings_appearance_pick_background_image_title {
get {
return ResourceManager.GetString("builtin_settings_appearance_pick_background_image_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} extensions found.
/// </summary>

View File

@@ -239,4 +239,7 @@
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
<value>{0} extensions installed</value>
</data>
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
<value>Pick background image</value>
</data>
</root>

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Provides theme-related values for the Command Palette and notifies listeners about
/// changes that affect visual appearance (theme, tint, background image, and backdrop).
/// </summary>
/// <remarks>
/// Implementations are expected to monitor system/app theme changes and raise
/// <see cref="ThemeChanged"/> accordingly. Consumers should call <see cref="Initialize"/>
/// once to hook required sources and then query properties/methods for the current visuals.
/// </remarks>
public interface IThemeService
{
/// <summary>
/// Occurs when the effective theme or any visual-affecting setting changes.
/// </summary>
/// <remarks>
/// Triggered for changes such as app theme (light/dark/default), background image,
/// tint/accent, or backdrop parameters that would require UI to refresh styling.
/// </remarks>
event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
/// <summary>
/// Initializes the theme service and starts listening for theme-related changes.
/// </summary>
/// <remarks>
/// Safe to call once during application startup before consuming the service.
/// </remarks>
void Initialize();
/// <summary>
/// Gets the current theme settings.
/// </summary>
ThemeSnapshot Current { get; }
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Event arguments for theme-related changes. </summary>
public class ThemeChangedEventArgs : EventArgs;

View File

@@ -0,0 +1,62 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Represents a snapshot of theme-related visual settings, including accent color, theme preference, and background
/// image configuration, for use in rendering the Command Palette UI.
/// </summary>
public sealed class ThemeSnapshot
{
/// <summary>
/// Gets the accent tint color used by the Command Palette visuals.
/// </summary>
public required Color Tint { get; init; }
/// <summary>
/// Gets the accent tint color used by the Command Palette visuals.
/// </summary>
public required float TintIntensity { get; init; }
/// <summary>
/// Gets the configured application theme preference.
/// </summary>
public required ElementTheme Theme { get; init; }
/// <summary>
/// Gets the image source to render as the background, if any.
/// </summary>
/// <remarks>
/// Returns <see langword="null"/> when no background image is configured.
/// </remarks>
public required ImageSource? BackgroundImageSource { get; init; }
/// <summary>
/// Gets the stretch mode used to lay out the background image.
/// </summary>
public required Stretch BackgroundImageStretch { get; init; }
/// <summary>
/// Gets the opacity applied to the background image.
/// </summary>
/// <value>
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
/// </value>
public required double BackgroundImageOpacity { get; init; }
/// <summary>
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
/// </summary>
/// <returns>The resolved <c>AcrylicBackdropParameters</c> to apply.</returns>
public required AcrylicBackdropParameters BackdropParameters { get; init; }
public required int BlurAmount { get; init; }
public required float BackgroundBrightness { get; init; }
}

View File

@@ -11,7 +11,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI;
using Windows.Foundation;
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -62,6 +64,24 @@ public partial class SettingsModel : ObservableObject
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
public UserTheme Theme { get; set; } = UserTheme.Default;
public ColorizationMode ColorizationMode { get; set; }
public Color CustomThemeColor { get; set; } = Colors.Transparent;
public int CustomThemeColorIntensity { get; set; } = 100;
public int BackgroundImageOpacity { get; set; } = 20;
public int BackgroundImageBlurAmount { get; set; }
public int BackgroundImageBrightness { get; set; }
public BackgroundImageFit BackgroundImageFit { get; set; }
public string? BackgroundImagePath { get; set; }
// END SETTINGS
///////////////////////////////////////////////////////////////////////////

View File

@@ -4,6 +4,8 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
@@ -29,6 +31,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
public AppearanceSettingsViewModel Appearance { get; }
public HotkeySettings? Hotkey
{
get => _settings.Hotkey;
@@ -179,6 +183,9 @@ public partial class SettingsViewModel : INotifyPropertyChanged
_settings = settings;
_serviceProvider = serviceProvider;
var themeService = serviceProvider.GetRequiredService<IThemeService>();
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
var activeProviders = GetCommandProviders();
var allProviderSettings = _settings.ProviderSettings;

View File

@@ -18,7 +18,7 @@ using WyHash;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class TopLevelViewModel : ObservableObject, IListItem
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
{
private readonly SettingsModel _settings;
private readonly ProviderSettings _providerSettings;
@@ -232,6 +232,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
UpdateInitialIcon();
}
else if (e.PropertyName == nameof(CommandItem.DataPackage))
{
DoOnUiThread(() =>
{
OnPropertyChanged(nameof(CommandItem.DataPackage));
});
}
}
}
@@ -394,4 +401,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}";
}
public IDictionary<string, object?> GetProperties()
{
return new Dictionary<string, object?>
{
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
};
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
public enum UserTheme
{
Default,
Light,
Dark,
}

View File

@@ -4,19 +4,23 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:local="using:Microsoft.CmdPal.UI">
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:services="using:Microsoft.CmdPal.UI.Services">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
<ResourceDictionary Source="Styles/Colors.xaml" />
<ResourceDictionary Source="Styles/TextBlock.xaml" />
<ResourceDictionary Source="Styles/TextBox.xaml" />
<ResourceDictionary Source="Styles/Settings.xaml" />
<ResourceDictionary Source="Controls/Tag.xaml" />
<ResourceDictionary Source="Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="Controls/IsEnabledTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
<!-- Default theme dictionary -->
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
<services:MutableOverridesDictionary />
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->

View File

@@ -24,9 +24,11 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
using Microsoft.CmdPal.Ext.WindowWalker;
using Microsoft.CmdPal.Ext.WinGet;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
@@ -112,6 +114,17 @@ public partial class App : Application
// Root services
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
AddBuiltInCommands(services);
AddCoreServices(services);
AddUIServices(services);
return services.BuildServiceProvider();
}
private static void AddBuiltInCommands(ServiceCollection services)
{
// Built-in Commands. Order matters - this is the order they'll be presented by default.
var allApps = new AllAppsCommandProvider();
var files = new IndexerCommandsProvider();
@@ -154,17 +167,32 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
}
private static void AddUIServices(ServiceCollection services)
{
// Models
services.AddSingleton<TopLevelCommandManager>();
services.AddSingleton<AliasManager>();
services.AddSingleton<HotkeyManager>();
var sm = SettingsModel.LoadSettings();
services.AddSingleton(sm);
var state = AppStateModel.LoadState();
services.AddSingleton(state);
services.AddSingleton<IExtensionService, ExtensionService>();
// Services
services.AddSingleton<TopLevelCommandManager>();
services.AddSingleton<AliasManager>();
services.AddSingleton<HotkeyManager>();
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<TrayIconService>();
services.AddSingleton<IThemeService, ThemeService>();
services.AddSingleton<ResourceSwapper>();
}
private static void AddCoreServices(ServiceCollection services)
{
// Core services
services.AddSingleton<IExtensionService, ExtensionService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
@@ -174,7 +202,5 @@ public partial class App : Application
// ViewModels
services.AddSingleton<ShellViewModel>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
return services.BuildServiceProvider();
}
}

View File

@@ -0,0 +1,449 @@
// 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.Numerics;
using ManagedCommon;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.Controls;
internal sealed partial class BlurImageControl : Control
{
private const string ImageSourceParameterName = "ImageSource";
private const string BrightnessEffectName = "Brightness";
private const string BrightnessOverlayEffectName = "BrightnessOverlay";
private const string BlurEffectName = "Blur";
private const string TintBlendEffectName = "TintBlend";
private const string TintEffectName = "Tint";
#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
#pragma warning restore CA1507
private static readonly string[] AnimatableProperties = [
BrightnessSource1AmountEffectProperty,
BrightnessSource2AmountEffectProperty,
BrightnessOverlayColorEffectProperty,
BlurBlurAmountEffectProperty,
TintColorEffectProperty
];
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register(
nameof(ImageSource),
typeof(ImageSource),
typeof(BlurImageControl),
new PropertyMetadata(null, OnImageChanged));
public static readonly DependencyProperty ImageStretchProperty =
DependencyProperty.Register(
nameof(ImageStretch),
typeof(Stretch),
typeof(BlurImageControl),
new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));
public static readonly DependencyProperty ImageOpacityProperty =
DependencyProperty.Register(
nameof(ImageOpacity),
typeof(double),
typeof(BlurImageControl),
new PropertyMetadata(1.0, OnOpacityChanged));
public static readonly DependencyProperty ImageBrightnessProperty =
DependencyProperty.Register(
nameof(ImageBrightness),
typeof(double),
typeof(BlurImageControl),
new PropertyMetadata(1.0, OnBrightnessChanged));
public static readonly DependencyProperty BlurAmountProperty =
DependencyProperty.Register(
nameof(BlurAmount),
typeof(double),
typeof(BlurImageControl),
new PropertyMetadata(0.0, OnBlurAmountChanged));
public static readonly DependencyProperty TintColorProperty =
DependencyProperty.Register(
nameof(TintColor),
typeof(Color),
typeof(BlurImageControl),
new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));
public static readonly DependencyProperty TintIntensityProperty =
DependencyProperty.Register(
nameof(TintIntensity),
typeof(double),
typeof(BlurImageControl),
new PropertyMetadata(0.0, OnVisualPropertyChanged));
private Compositor? _compositor;
private SpriteVisual? _effectVisual;
private CompositionEffectBrush? _effectBrush;
private CompositionSurfaceBrush? _imageBrush;
public BlurImageControl()
{
this.DefaultStyleKey = typeof(BlurImageControl);
this.Loaded += OnLoaded;
this.SizeChanged += OnSizeChanged;
}
public ImageSource ImageSource
{
get => (ImageSource)GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
public Stretch ImageStretch
{
get => (Stretch)GetValue(ImageStretchProperty);
set => SetValue(ImageStretchProperty, value);
}
public double ImageOpacity
{
get => (double)GetValue(ImageOpacityProperty);
set => SetValue(ImageOpacityProperty, value);
}
public double ImageBrightness
{
get => (double)GetValue(ImageBrightnessProperty);
set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
}
public double BlurAmount
{
get => (double)GetValue(BlurAmountProperty);
set => SetValue(BlurAmountProperty, value);
}
public Color TintColor
{
get => (Color)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public double TintIntensity
{
get => (double)GetValue(TintIntensityProperty);
set => SetValue(TintIntensityProperty, value);
}
private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BlurImageControl control && control._imageBrush != null)
{
control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
}
}
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BlurImageControl control && control._compositor != null)
{
control.UpdateEffect();
}
}
private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BlurImageControl control && control._effectVisual != null)
{
control._effectVisual.Opacity = (float)(double)e.NewValue;
}
}
private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BlurImageControl control && control._effectBrush != null)
{
control.UpdateEffect();
}
}
private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BlurImageControl control && control._effectBrush != null)
{
control.UpdateEffect();
}
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
InitializeComposition();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (_effectVisual != null)
{
_effectVisual.Size = new Vector2(
(float)Math.Max(1, e.NewSize.Width),
(float)Math.Max(1, e.NewSize.Height));
}
}
private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not BlurImageControl control)
{
return;
}
control.EnsureEffect(force: true);
control.UpdateEffect();
}
private void InitializeComposition()
{
var visual = ElementCompositionPreview.GetElementVisual(this);
_compositor = visual.Compositor;
_effectVisual = _compositor.CreateSpriteVisual();
_effectVisual.Size = new Vector2(
(float)Math.Max(1, ActualWidth),
(float)Math.Max(1, ActualHeight));
_effectVisual.Opacity = (float)ImageOpacity;
ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);
UpdateEffect();
}
private void EnsureEffect(bool force = false)
{
if (_compositor is null)
{
return;
}
if (_effectBrush is not null && !force)
{
return;
}
var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);
// 1) Brightness via ArithmeticCompositeEffect
// We blend between the original image and either black or white,
// depending on whether we want to darken or brighten. BrightnessEffect isn't supported
// in the composition graph.
var brightnessEffect = new ArithmeticCompositeEffect
{
Name = BrightnessEffectName,
Source1 = imageSource, // original image
Source2 = new ColorSourceEffect
{
Name = BrightnessOverlayEffectName,
Color = Colors.Black, // we'll swap black/white via properties
},
MultiplyAmount = 0.0f,
Source1Amount = 1.0f, // original
Source2Amount = 0.0f, // overlay
Offset = 0.0f,
};
// 2) Blur
var blurEffect = new GaussianBlurEffect
{
Name = BlurEffectName,
BlurAmount = 0.0f,
BorderMode = EffectBorderMode.Hard,
Optimization = EffectOptimization.Balanced,
Source = brightnessEffect,
};
// 3) Tint (always in the chain; intensity via alpha)
var tintEffect = new BlendEffect
{
Name = TintBlendEffectName,
Background = blurEffect,
Foreground = new ColorSourceEffect
{
Name = TintEffectName,
Color = Colors.Transparent,
},
Mode = BlendEffectMode.Multiply,
};
var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);
_effectBrush?.Dispose();
_effectBrush = effectFactory.CreateBrush();
// Set initial source
if (ImageSource is not null)
{
_imageBrush ??= _compositor.CreateSurfaceBrush();
LoadImageAsync(ImageSource);
_effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
}
else
{
_effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
}
if (_effectVisual is not null)
{
_effectVisual.Brush = _effectBrush;
}
}
private void UpdateEffect()
{
if (_compositor is null)
{
return;
}
EnsureEffect();
if (_effectBrush is null)
{
return;
}
var props = _effectBrush.Properties;
// Brightness
var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);
float source1Amount;
float source2Amount;
Color overlayColor;
if (b >= 0)
{
// Brighten: blend towards white
overlayColor = Colors.White;
source1Amount = 1.0f - b; // original image contribution
source2Amount = b; // white overlay contribution
}
else
{
// Darken: blend towards black
overlayColor = Colors.Black;
var t = -b; // 0..1
source1Amount = 1.0f - t; // original image
source2Amount = t; // black overlay
}
props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);
// Blur
props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);
// Tint
var tintColor = TintColor;
var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);
var adjustedColor = Color.FromArgb(
(byte)(clampedIntensity * 255),
tintColor.R,
tintColor.G,
tintColor.B);
props.InsertColor(TintColorEffectProperty, adjustedColor);
}
private void LoadImageAsync(ImageSource imageSource)
{
try
{
if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
{
return;
}
_imageBrush ??= _compositor?.CreateSurfaceBrush();
if (_imageBrush is null)
{
return;
}
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
SetLoadedSurfaceToBrush(loadedSurface);
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
}
catch (Exception ex)
{
Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
}
return;
void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
{
switch (e.Status)
{
case LoadedImageSourceLoadStatus.Success:
Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");
try
{
SetLoadedSurfaceToBrush(loadedSurface);
}
catch (Exception ex)
{
Logger.LogError("Failed to set surface in BlurImageControl", ex);
throw;
}
break;
case LoadedImageSourceLoadStatus.NetworkError:
case LoadedImageSourceLoadStatus.InvalidFormat:
case LoadedImageSourceLoadStatus.Other:
default:
Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
break;
}
}
}
private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
{
var surfaceBrush = _imageBrush;
if (surfaceBrush is null)
{
return;
}
surfaceBrush.Surface = loadedSurface;
surfaceBrush.Stretch = ConvertStretch(ImageStretch);
surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
}
private static CompositionStretch ConvertStretch(Stretch stretch)
{
return stretch switch
{
Stretch.None => CompositionStretch.None,
Stretch.Fill => CompositionStretch.Fill,
Stretch.Uniform => CompositionStretch.Uniform,
Stretch.UniformToFill => CompositionStretch.UniformToFill,
_ => CompositionStretch.UniformToFill,
};
}
private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
}

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ColorPalette"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:localConverters="using:Microsoft.CmdPal.UI.Converters"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkitConverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<UserControl.Resources>
<toolkitConverters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<localConverters:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<Style x:Key="PaletteGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="TabNavigation" Value="Local" />
<Setter Property="IsHoldingEnabled" Value="True" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="MinWidth" Value="32" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="AllowDrop" Value="False" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<Grid
x:Name="ContentBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<ScaleTransform x:Name="ContentBorderScale" />
</Grid.RenderTransform>
<ContentPresenter
x:Name="ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<!--
The 'Xg' text simulates the amount of space one line of text will occupy.
In the DataPlaceholder state, the Content is not loaded yet so we
approximate the size of the item using placeholder text.
-->
<TextBlock
x:Name="PlaceholderTextBlock"
Margin="{TemplateBinding Padding}"
AutomationProperties.AccessibilityView="Raw"
Foreground="{x:Null}"
IsHitTestVisible="False"
Text="Xg"
Visibility="Collapsed" />
<Rectangle
x:Name="PlaceholderRect"
Fill="{ThemeResource ListViewItemPlaceholderBackground}"
Visibility="Collapsed" />
<Rectangle
x:Name="BorderRectangle"
IsHitTestVisible="False"
Opacity="0"
RadiusX="6"
RadiusY="6"
Stroke="{ThemeResource SystemControlHighlightListAccentLowBrush}"
StrokeThickness="2" />
<Border
x:Name="MultiSelectSquare"
Width="20"
Height="20"
Margin="0,2,2,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{ThemeResource SystemControlBackgroundChromeMediumBrush}"
CornerRadius="6"
Visibility="Collapsed">
<FontIcon
x:Name="MultiSelectCheck"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumHighBrush}"
Glyph="&#xE73E;"
Opacity="0" />
</Border>
<Border
x:Name="MultiArrangeOverlayTextBorder"
Height="20"
MinWidth="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource SystemControlBackgroundAccentBrush}"
BorderBrush="{ThemeResource SystemControlBackgroundChromeWhiteBrush}"
BorderThickness="2"
CornerRadius="6"
IsHitTestVisible="False"
Opacity="0">
<TextBlock
x:Name="MultiArrangeOverlayText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
IsHitTestVisible="False"
Opacity="0"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.DragItemsCount}" />
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused" />
</VisualStateGroup>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="BorderRectangle"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="2" />
</ObjectAnimationUsingKeyFrames>
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
<DoubleAnimation
Storyboard.TargetName="MultiSelectCheck"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="MultiSelectSquare" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid HorizontalAlignment="Stretch">
<GridView
Margin="0"
Padding="0"
IsItemClickEnabled="True"
ItemClick="ListViewBase_OnItemClick"
ItemContainerStyle="{StaticResource PaletteGridViewItemStyle}"
ItemsSource="{x:Bind PaletteColors}"
SelectionMode="None">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<controls:UniformGrid ui:FrameworkElementExtensions.AncestorType="local:ColorPalette" Columns="{Binding (ui:FrameworkElementExtensions.Ancestor).CustomPaletteColumnCount, RelativeSource={RelativeSource Self}}" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate x:DataType="Color">
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
CornerRadius="4"
ToolTipService.ToolTip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
</UserControl>

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.UI;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ColorPalette : UserControl
{
public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection<Color>), typeof(ColorPalette), null!)!;
public static readonly DependencyProperty CustomPaletteColumnCountProperty = DependencyProperty.Register(nameof(CustomPaletteColumnCount), typeof(int), typeof(ColorPalette), new PropertyMetadata(10))!;
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPalette), new PropertyMetadata(null!))!;
public event EventHandler<Color?>? SelectedColorChanged;
private Color? _selectedColor;
public Color? SelectedColor
{
get => _selectedColor;
set
{
if (_selectedColor != value)
{
_selectedColor = value;
if (value is not null)
{
SetValue(SelectedColorProperty, value);
}
else
{
ClearValue(SelectedColorProperty);
}
}
}
}
public ObservableCollection<Color> PaletteColors
{
get => (ObservableCollection<Color>)GetValue(PaletteColorsProperty)!;
set => SetValue(PaletteColorsProperty, value);
}
public int CustomPaletteColumnCount
{
get => (int)GetValue(CustomPaletteColumnCountProperty);
set => SetValue(CustomPaletteColumnCountProperty, value);
}
public ColorPalette()
{
PaletteColors = [];
InitializeComponent();
}
private void ListViewBase_OnItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is Color color)
{
SelectedColor = color;
SelectedColorChanged?.Invoke(this, color);
}
}
}

View File

@@ -0,0 +1,90 @@
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ColorPickerButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<UserControl.Resources>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolToVisibilityConverter
x:Key="BoolToVisibilityInvertedConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
</UserControl.Resources>
<StackPanel Orientation="Horizontal" Spacing="8">
<DropDownButton Padding="{x:Bind ToDropDownPadding(HasSelectedColor), Mode=OneWay}">
<Grid>
<TextBlock x:Uid="OptionalColorPickerButton_UnsetTextBlock" Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityInvertedConverter}}" />
<Border
x:Name="ColorPreviewBorder"
Width="48"
Height="24"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}"
Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Border.Background>
<SolidColorBrush Color="{x:Bind SelectedColor, Mode=OneWay}" />
</Border.Background>
</Border>
</Grid>
<DropDownButton.Flyout>
<Flyout
x:Name="ColorPickerFlyout"
Opened="FlyoutBase_OnOpened"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="660" />
</Style>
</Flyout.FlyoutPresenterStyle>
<StackPanel
x:Name="FlyoutRoot"
Orientation="Horizontal"
SizeChanged="FlyoutRoot_OnSizeChanged"
Spacing="20">
<!-- Left column: Preset colors and reset button -->
<StackPanel Margin="2">
<TextBlock
x:Uid="OptionalColorPickerButton_WindowsColorsSectionHeading"
Margin="0,0,0,12"
Style="{StaticResource BodyTextBlockStyle}" />
<controls:ColorPalette
HorizontalAlignment="Left"
VerticalAlignment="Top"
CustomPaletteColumnCount="9"
PaletteColors="{x:Bind PaletteColors}"
SelectedColorChanged="ColorPalette_OnSelectedColorChanged" />
</StackPanel>
<!-- Right column: Spectrum -->
<StackPanel>
<TextBlock
x:Uid="OptionalColorPickerButton_CustomColorsSectionHeading"
Margin="0,0,0,12"
Style="{StaticResource BodyTextBlockStyle}" />
<ColorPicker
IsAlphaEnabled="{x:Bind IsAlphaEnabled, Mode=OneWay}"
IsAlphaSliderVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
IsAlphaTextInputVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
IsColorChannelTextInputVisible="True"
IsColorSliderVisible="True"
IsHexInputVisible="True"
IsMoreButtonVisible="True"
Color="{x:Bind SelectedColor, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,146 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.UI;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ColorPickerButton : UserControl
{
public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection<Color>), typeof(ColorPickerButton), new PropertyMetadata(new ObservableCollection<Color>()))!;
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerButton), new PropertyMetadata(Colors.Black))!;
public static readonly DependencyProperty IsAlphaEnabledProperty = DependencyProperty.Register(nameof(IsAlphaEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(defaultValue: false))!;
public static readonly DependencyProperty IsValueEditorEnabledProperty = DependencyProperty.Register(nameof(IsValueEditorEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
public static readonly DependencyProperty HasSelectedColorProperty = DependencyProperty.Register(nameof(HasSelectedColor), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
private Color _selectedColor;
public Color SelectedColor
{
get
{
return _selectedColor;
}
set
{
if (_selectedColor != value)
{
_selectedColor = value;
SetValue(SelectedColorProperty, value);
HasSelectedColor = true;
}
}
}
public bool HasSelectedColor
{
get { return (bool)GetValue(HasSelectedColorProperty); }
set { SetValue(HasSelectedColorProperty, value); }
}
public bool IsAlphaEnabled
{
get => (bool)GetValue(IsAlphaEnabledProperty);
set => SetValue(IsAlphaEnabledProperty, value);
}
public bool IsValueEditorEnabled
{
get { return (bool)GetValue(IsValueEditorEnabledProperty); }
set { SetValue(IsValueEditorEnabledProperty, value); }
}
public ObservableCollection<Color> PaletteColors
{
get { return (ObservableCollection<Color>)GetValue(PaletteColorsProperty); }
set { SetValue(PaletteColorsProperty, value); }
}
public ColorPickerButton()
{
this.InitializeComponent();
IsEnabledChanged -= ColorPickerButton_IsEnabledChanged;
SetEnabledState();
IsEnabledChanged += ColorPickerButton_IsEnabledChanged;
}
private void ColorPickerButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetEnabledState();
}
private void SetEnabledState()
{
if (this.IsEnabled)
{
ColorPreviewBorder.Opacity = 1;
}
else
{
ColorPreviewBorder.Opacity = 0.2;
}
}
private void ColorPalette_OnSelectedColorChanged(object? sender, Color? e)
{
if (e.HasValue)
{
HasSelectedColor = true;
SelectedColor = e.Value;
}
}
private void FlyoutBase_OnOpened(object? sender, object e)
{
if (sender is not Flyout flyout || (flyout.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
{
return;
}
FlyoutRoot!.UpdateLayout();
flyoutPresenter.UpdateLayout();
// Logger.LogInfo($"FlyoutBase_OnOpened: {flyoutPresenter}, {FlyoutRoot!.ActualWidth}");
flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
flyoutPresenter.MinWidth = 660;
flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
}
private void FlyoutRoot_OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if ((ColorPickerFlyout!.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
{
return;
}
FlyoutRoot!.UpdateLayout();
flyoutPresenter.UpdateLayout();
flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
flyoutPresenter.MinWidth = 660;
flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
}
private Thickness ToDropDownPadding(bool hasColor)
{
return hasColor ? new Thickness(3, 3, 8, 3) : new Thickness(8, 4, 8, 4);
}
private void ResetButton_Click(object sender, RoutedEventArgs e)
{
HasSelectedColor = false;
ColorPickerFlyout?.Hide();
}
}

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.CommandPalettePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Border
Width="200"
Height="120"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Translation="0,0,8">
<Grid>
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
<Border.Background>
<AcrylicBrush
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
</Border.Background>
</Border>
<local:BlurImageControl
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
IsHitTestVisible="False"
TintColor="{x:Bind PreviewBackgroundImageTint, Mode=OneWay}"
TintIntensity="{x:Bind ToTintIntensity(PreviewBackgroundImageTintIntensity), Mode=OneWay}"
Visibility="{x:Bind ShowBackgroundImage, Mode=OneWay}" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Border
x:Name="ContentPreview"
Grid.Row="0"
Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}" BorderThickness="0,0,0,1" />
</Grid>
</Border>
<Border
x:Name="CommandBarPreview"
Grid.Row="1"
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
BorderBrush="{ThemeResource CmdPal.CommandBarBorderBrush}"
BorderThickness="0,1,0,0" />
</Grid>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandPalettePreview : UserControl
{
public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback));
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
public static readonly DependencyProperty PreviewBackgroundImageBrightnessProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBrightness), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
public static readonly DependencyProperty PreviewBackgroundImageBlurAmountProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBlurAmount), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
public static readonly DependencyProperty PreviewBackgroundImageTintProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTint), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
public BackgroundImageFit PreviewBackgroundImageFit
{
get { return (BackgroundImageFit)GetValue(PreviewBackgroundImageFitProperty); }
set { SetValue(PreviewBackgroundImageFitProperty, value); }
}
public double PreviewBackgroundOpacity
{
get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
set { SetValue(PreviewBackgroundOpacityProperty, value); }
}
public Color PreviewBackgroundColor
{
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
set { SetValue(PreviewBackgroundColorProperty, value); }
}
public ImageSource PreviewBackgroundImageSource
{
get { return (ImageSource)GetValue(PreviewBackgroundImageSourceProperty); }
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
}
public int PreviewBackgroundImageOpacity
{
get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
}
public double PreviewBackgroundImageBrightness
{
get => (double)GetValue(PreviewBackgroundImageBrightnessProperty);
set => SetValue(PreviewBackgroundImageBrightnessProperty, value);
}
public double PreviewBackgroundImageBlurAmount
{
get => (double)GetValue(PreviewBackgroundImageBlurAmountProperty);
set => SetValue(PreviewBackgroundImageBlurAmountProperty, value);
}
public Color PreviewBackgroundImageTint
{
get => (Color)GetValue(PreviewBackgroundImageTintProperty);
set => SetValue(PreviewBackgroundImageTintProperty, value);
}
public int PreviewBackgroundImageTintIntensity
{
get => (int)GetValue(PreviewBackgroundImageTintIntensityProperty);
set => SetValue(PreviewBackgroundImageTintIntensityProperty, value);
}
public Visibility ShowBackgroundImage
{
get => (Visibility)GetValue(ShowBackgroundImageProperty);
set => SetValue(ShowBackgroundImageProperty, value);
}
public CommandPalettePreview()
{
InitializeComponent();
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not CommandPalettePreview preview)
{
return;
}
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
}
private double ToOpacity(int value) => value / 100.0;
private double ToTintIntensity(int value) => value / 100.0;
private Stretch ToStretch(BackgroundImageFit fit)
{
return fit switch
{
BackgroundImageFit.Fill => Stretch.Fill,
BackgroundImageFit.UniformToFill => Stretch.UniformToFill,
_ => Stretch.None,
};
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ScreenPreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Border
x:Name="ScreenBorder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
BorderBrush="Black"
BorderThickness="8"
CornerRadius="8"
UseLayoutRounding="True">
<Viewbox Height="120" UseLayoutRounding="True">
<Grid>
<Image
x:Name="WallpaperImage"
MaxHeight="200"
Stretch="Uniform"
UseLayoutRounding="True" />
<ContentPresenter
x:Name="ContentPresenter"
Margin="40"
Content="{x:Bind PreviewContent}"
UseLayoutRounding="True" />
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
namespace Microsoft.CmdPal.UI.Controls;
[ContentProperty(Name = nameof(PreviewContent))]
public sealed partial class ScreenPreview : UserControl
{
public static readonly DependencyProperty PreviewContentProperty =
DependencyProperty.Register(nameof(PreviewContent), typeof(object), typeof(ScreenPreview), new PropertyMetadata(null!))!;
public object PreviewContent
{
get => GetValue(PreviewContentProperty)!;
set => SetValue(PreviewContentProperty, value);
}
public ScreenPreview()
{
InitializeComponent();
var wallpaperHelper = new WallpaperHelper();
WallpaperImage!.Source = wallpaperHelper.GetWallpaperImage()!;
ScreenBorder!.Background = new SolidColorBrush(wallpaperHelper.GetWallpaperColor());
}
}

View File

@@ -4,9 +4,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUi="using:Microsoft.CmdPal.UI"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
@@ -22,6 +21,7 @@
MinHeight="32"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
h:TextBoxCaretColor.SyncWithForeground="True"
AutomationProperties.AutomationId="MainSearchBox"
KeyDown="FilterBox_KeyDown"
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"

View File

@@ -379,6 +379,12 @@ public sealed partial class SearchBar : UserControl,
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
// Telemetry: Track search query count for session metrics (only non-empty queries)
if (!string.IsNullOrWhiteSpace(FilterBox.Text))
{
WeakReferenceMessenger.Default.Send<SearchQueryMessage>(new());
}
}
}

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 Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.Controls;
internal sealed class UVBounds
{
public double UMin { get; }
public double UMax { get; }
public double VMin { get; }
public double VMax { get; }
public UVBounds(Orientation orientation, Rect rect)
{
if (orientation == Orientation.Horizontal)
{
UMin = rect.Left;
UMax = rect.Right;
VMin = rect.Top;
VMax = rect.Bottom;
}
else
{
UMin = rect.Top;
UMax = rect.Bottom;
VMin = rect.Left;
VMax = rect.Right;
}
}
}

View File

@@ -0,0 +1,96 @@
// 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;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.Controls;
[DebuggerDisplay("U = {U} V = {V}")]
internal struct UvMeasure
{
internal double U { get; set; }
internal double V { get; set; }
internal static UvMeasure Zero => default(UvMeasure);
public UvMeasure(Orientation orientation, Size size)
: this(orientation, size.Width, size.Height)
{
}
public UvMeasure(Orientation orientation, double width, double height)
{
if (orientation == Orientation.Horizontal)
{
U = width;
V = height;
}
else
{
U = height;
V = width;
}
}
public UvMeasure Add(double u, double v)
{
UvMeasure result = default(UvMeasure);
result.U = U + u;
result.V = V + v;
return result;
}
public UvMeasure Add(UvMeasure measure)
{
return Add(measure.U, measure.V);
}
public Size ToSize(Orientation orientation)
{
if (orientation != Orientation.Horizontal)
{
return new Size(V, U);
}
return new Size(U, V);
}
public Point GetPoint(Orientation orientation)
{
return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U);
}
public Size GetSize(Orientation orientation)
{
return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U);
}
public static bool operator ==(UvMeasure measure1, UvMeasure measure2)
{
return measure1.U == measure2.U && measure1.V == measure2.V;
}
public static bool operator !=(UvMeasure measure1, UvMeasure measure2)
{
return !(measure1 == measure2);
}
public override bool Equals(object? obj)
{
return obj is UvMeasure measure && this == measure;
}
public bool Equals(UvMeasure value)
{
return this == value;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}

View File

@@ -0,0 +1,418 @@
// 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 CommunityToolkit.WinUI.Controls;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
using ToolkitStretchChild = CommunityToolkit.WinUI.Controls.StretchChild;
namespace Microsoft.CmdPal.UI.Controls;
/// <summary>
/// Arranges elements by wrapping them to fit the available space.
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
/// </summary>
public sealed partial class WrapPanel : Panel
{
private struct UvRect
{
public UvMeasure Position { get; set; }
public UvMeasure Size { get; set; }
public Rect ToRect(Orientation orientation)
{
return orientation switch
{
Orientation.Vertical => new Rect(Position.V, Position.U, Size.V, Size.U),
Orientation.Horizontal => new Rect(Position.U, Position.V, Size.U, Size.V),
_ => ThrowArgumentException(),
};
}
private static Rect ThrowArgumentException()
{
throw new ArgumentException("The input orientation is not valid.");
}
}
private struct Row
{
public List<UvRect> ChildrenRects { get; }
public UvMeasure Size { get; set; }
public UvRect Rect
{
get
{
UvRect result;
if (ChildrenRects.Count <= 0)
{
result = default(UvRect);
result.Position = UvMeasure.Zero;
result.Size = Size;
return result;
}
result = default(UvRect);
result.Position = ChildrenRects.First().Position;
result.Size = Size;
return result;
}
}
public Row(List<UvRect> childrenRects, UvMeasure size)
{
ChildrenRects = childrenRects;
Size = size;
}
public void Add(UvMeasure position, UvMeasure size)
{
ChildrenRects.Add(new UvRect
{
Position = position,
Size = size,
});
Size = new UvMeasure
{
U = position.U + size.U,
V = Math.Max(Size.V, size.V),
};
}
}
/// <summary>
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
/// </summary>
public double HorizontalSpacing
{
get { return (double)GetValue(HorizontalSpacingProperty); }
set { SetValue(HorizontalSpacingProperty, value); }
}
private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator;
/// <summary>
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
/// </summary>
public static readonly DependencyProperty HorizontalSpacingProperty =
DependencyProperty.Register(
nameof(HorizontalSpacing),
typeof(double),
typeof(WrapPanel),
new PropertyMetadata(0d, LayoutPropertyChanged));
/// <summary>
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
/// </summary>
public double VerticalSpacing
{
get { return (double)GetValue(VerticalSpacingProperty); }
set { SetValue(VerticalSpacingProperty, value); }
}
/// <summary>
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
/// </summary>
public static readonly DependencyProperty VerticalSpacingProperty =
DependencyProperty.Register(
nameof(VerticalSpacing),
typeof(double),
typeof(WrapPanel),
new PropertyMetadata(0d, LayoutPropertyChanged));
/// <summary>
/// Gets or sets the orientation of the WrapPanel.
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
/// </summary>
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
/// <summary>
/// Identifies the <see cref="Orientation"/> dependency property.
/// </summary>
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register(
nameof(Orientation),
typeof(Orientation),
typeof(WrapPanel),
new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged));
/// <summary>
/// Gets or sets the distance between the border and its child object.
/// </summary>
/// <returns>
/// The dimensions of the space between the border and its child as a Thickness value.
/// Thickness is a structure that stores dimension values using pixel measures.
/// </returns>
public Thickness Padding
{
get { return (Thickness)GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
/// <summary>
/// Identifies the Padding dependency property.
/// </summary>
/// <returns>The identifier for the <see cref="Padding"/> dependency property.</returns>
public static readonly DependencyProperty PaddingProperty =
DependencyProperty.Register(
nameof(Padding),
typeof(Thickness),
typeof(WrapPanel),
new PropertyMetadata(default(Thickness), LayoutPropertyChanged));
/// <summary>
/// Gets or sets a value indicating how to arrange child items
/// </summary>
public ToolkitStretchChild StretchChild
{
get { return (ToolkitStretchChild)GetValue(StretchChildProperty); }
set { SetValue(StretchChildProperty, value); }
}
/// <summary>
/// Identifies the <see cref="StretchChild"/> dependency property.
/// </summary>
/// <returns>The identifier for the <see cref="StretchChild"/> dependency property.</returns>
public static readonly DependencyProperty StretchChildProperty =
DependencyProperty.Register(
nameof(StretchChild),
typeof(ToolkitStretchChild),
typeof(WrapPanel),
new PropertyMetadata(ToolkitStretchChild.None, LayoutPropertyChanged));
/// <summary>
/// Identifies the IsFullLine attached dependency property.
/// If true, the child element will occupy the entire width of the panel and force a line break before and after itself.
/// </summary>
public static readonly DependencyProperty IsFullLineProperty =
DependencyProperty.RegisterAttached(
"IsFullLine",
typeof(bool),
typeof(WrapPanel),
new PropertyMetadata(false, OnIsFullLineChanged));
public static bool GetIsFullLine(DependencyObject obj)
{
return (bool)obj.GetValue(IsFullLineProperty);
}
public static void SetIsFullLine(DependencyObject obj, bool value)
{
obj.SetValue(IsFullLineProperty, value);
}
private static void OnIsFullLineChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (FindVisualParentWrapPanel(d) is WrapPanel wp)
{
wp.InvalidateMeasure();
}
}
private static WrapPanel? FindVisualParentWrapPanel(DependencyObject child)
{
var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(child);
while (parent != null)
{
if (parent is WrapPanel wrapPanel)
{
return wrapPanel;
}
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
}
return null;
}
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WrapPanel wp)
{
wp.InvalidateMeasure();
wp.InvalidateArrange();
}
}
private readonly List<Row> _rows = new List<Row>();
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
var childAvailableSize = new Size(
availableSize.Width - Padding.Left - Padding.Right,
availableSize.Height - Padding.Top - Padding.Bottom);
foreach (var child in Children)
{
child.Measure(childAvailableSize);
}
var requiredSize = UpdateRows(availableSize);
return requiredSize;
}
/// <inheritdoc />
protected override Size ArrangeOverride(Size finalSize)
{
if ((Orientation == Orientation.Horizontal && finalSize.Width < DesiredSize.Width) ||
(Orientation == Orientation.Vertical && finalSize.Height < DesiredSize.Height))
{
// We haven't received our desired size. We need to refresh the rows.
UpdateRows(finalSize);
}
if (_rows.Count > 0)
{
// Now that we have all the data, we do the actual arrange pass
var childIndex = 0;
foreach (var row in _rows)
{
foreach (var rect in row.ChildrenRects)
{
var child = Children[childIndex++];
while (child.Visibility == Visibility.Collapsed)
{
// Collapsed children are not added into the rows,
// we skip them.
child = Children[childIndex++];
}
var arrangeRect = new UvRect
{
Position = rect.Position,
Size = new UvMeasure { U = rect.Size.U, V = row.Size.V },
};
var finalRect = arrangeRect.ToRect(Orientation);
child.Arrange(finalRect);
}
}
}
return finalSize;
}
private Size UpdateRows(Size availableSize)
{
_rows.Clear();
var paddingStart = new UvMeasure(Orientation, Padding.Left, Padding.Top);
var paddingEnd = new UvMeasure(Orientation, Padding.Right, Padding.Bottom);
if (Children.Count == 0)
{
return paddingStart.Add(paddingEnd).ToSize(Orientation);
}
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
var position = new UvMeasure(Orientation, Padding.Left, Padding.Top);
var currentRow = new Row(new List<UvRect>(), default);
var finalMeasure = new UvMeasure(Orientation, width: 0.0, height: 0.0);
void CommitRow()
{
// Only adds if the row has a content
if (currentRow.ChildrenRects.Count > 0)
{
_rows.Add(currentRow);
position.V += currentRow.Size.V + spacingMeasure.V;
}
position.U = paddingStart.U;
currentRow = new Row(new List<UvRect>(), default);
}
void Arrange(UIElement child, bool isLast = false)
{
if (child.Visibility == Visibility.Collapsed)
{
return;
}
var isFullLine = IsSectionItem(child);
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
if (isFullLine)
{
if (currentRow.ChildrenRects.Count > 0)
{
CommitRow();
}
// Forces the width to fill all the available space
// (Total width - Padding Left - Padding Right)
desiredMeasure.U = parentMeasure.U - paddingStart.U - paddingEnd.U;
// Adds the Section Header to the row
currentRow.Add(position, desiredMeasure);
// Updates the global measures
position.U += desiredMeasure.U + spacingMeasure.U;
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
CommitRow();
}
else
{
// Checks if the item can fit in the row
if ((desiredMeasure.U + position.U + paddingEnd.U) > parentMeasure.U)
{
CommitRow();
}
if (isLast)
{
desiredMeasure.U = parentMeasure.U - position.U;
}
currentRow.Add(position, desiredMeasure);
position.U += desiredMeasure.U + spacingMeasure.U;
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
}
}
var lastIndex = Children.Count - 1;
for (var i = 0; i < lastIndex; i++)
{
Arrange(Children[i]);
}
Arrange(Children[lastIndex], StretchChild == ToolkitStretchChild.Last);
if (currentRow.ChildrenRects.Count > 0)
{
_rows.Add(currentRow);
}
if (_rows.Count == 0)
{
return paddingStart.Add(paddingEnd).ToSize(Orientation);
}
var lastRowRect = _rows.Last().Rect;
finalMeasure.V = lastRowRect.Position.V + lastRowRect.Size.V;
return finalMeasure.Add(paddingEnd).ToSize(Orientation);
}
}

View File

@@ -0,0 +1,121 @@
// 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.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.Converters;
/// <summary>
/// Gets a color, either black or white, depending on the brightness of the supplied color.
/// </summary>
public sealed partial class ContrastBrushConverter : IValueConverter
{
/// <summary>
/// Gets or sets the alpha channel threshold below which a default color is used instead of black/white.
/// </summary>
public byte AlphaThreshold { get; set; } = 128;
/// <inheritdoc />
public object Convert(
object value,
Type targetType,
object parameter,
string language)
{
Color comparisonColor;
Color? defaultColor = null;
// Get the changing color to compare against
if (value is Color valueColor)
{
comparisonColor = valueColor;
}
else if (value is SolidColorBrush valueBrush)
{
comparisonColor = valueBrush.Color;
}
else
{
// Invalid color value provided
return DependencyProperty.UnsetValue;
}
// Get the default color when transparency is high
if (parameter is Color parameterColor)
{
defaultColor = parameterColor;
}
else if (parameter is SolidColorBrush parameterBrush)
{
defaultColor = parameterBrush.Color;
}
if (comparisonColor.A < AlphaThreshold &&
defaultColor.HasValue)
{
// If the transparency is less than 50 %, just use the default brush
// This can commonly be something like the TextControlForeground brush
return new SolidColorBrush(defaultColor.Value);
}
else
{
// Chose a white/black brush based on contrast to the base color
return UseLightContrastColor(comparisonColor)
? new SolidColorBrush(Colors.White)
: new SolidColorBrush(Colors.Black);
}
}
/// <inheritdoc />
public object ConvertBack(
object value,
Type targetType,
object parameter,
string language)
{
return DependencyProperty.UnsetValue;
}
/// <summary>
/// Determines whether a light or dark contrast color should be used with the given displayed color.
/// </summary>
/// <remarks>
/// This code is using the WinUI algorithm.
/// </remarks>
private bool UseLightContrastColor(Color displayedColor)
{
// The selection ellipse should be light if and only if the chosen color
// contrasts more with black than it does with white.
// To find how much something contrasts with white, we use the equation
// for relative luminance, which is given by
//
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
//
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
//
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
// then the color is closer to black. This is based on the fact that the human
// eye perceives green to be much brighter than red, which in turn is perceived to be
// brighter than blue.
//
// If the third dimension is value, then we won't be updating the spectrum's displayed colors,
// so in that case we should use a value of 1 when considering the backdrop
// for the selection ellipse.
var rg = displayedColor.R <= 10
? displayedColor.R / 3294.0
: Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4);
var gg = displayedColor.G <= 10
? displayedColor.G / 3294.0
: Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4);
var bg = displayedColor.B <= 10
? displayedColor.B / 3294.0
: Math.Pow((displayedColor.B / 269.0) + 0.0513, 2.4);
return (0.2126 * rg) + (0.7152 * gg) + (0.0722 * bg) <= 0.5;
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.CmdPal.UI;
public partial class DetailsSizeToGridLengthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is ContentSize size)
{
// This converter calculates the Star width for the LIST.
//
// The input 'size' (ContentSize) represents the TARGET WIDTH desired for the DETAILS PANEL.
//
// To ensure the Details Panel achieves its target size (e.g. ContentSize.Large),
// we must shrink the List and let the Details fill the available space.
// (e.g., A larger target size for Details results in a smaller Star value for the List).
var starValue = size switch
{
ContentSize.Small => 3.0,
ContentSize.Medium => 2.0,
ContentSize.Large => 1.0,
_ => 3.0,
};
return new GridLength(starValue, GridUnitType.Star);
}
return new GridLength(3.0, GridUnitType.Star);
}
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}

View File

@@ -18,8 +18,23 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
public DataTemplate? Gallery { get; set; }
public DataTemplate? Section { get; set; }
public DataTemplate? Separator { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
if (item is ListItemViewModel element && element.IsSectionOrSeparator)
{
if (dependencyObject is UIElement li)
{
li.IsTabStop = false;
li.IsHitTestVisible = false;
}
return string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
}
return GridProperties switch
{
SmallGridPropertiesViewModel => Small,

View File

@@ -0,0 +1,47 @@
// 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.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
public sealed partial class ListItemTemplateSelector : DataTemplateSelector
{
public DataTemplate? ListItem { get; set; }
public DataTemplate? Separator { get; set; }
public DataTemplate? Section { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
{
DataTemplate? dataTemplate = ListItem;
if (container is ListViewItem listItem)
{
if (item is ListItemViewModel element)
{
if (container is ListViewItem li && element.IsSectionOrSeparator)
{
li.IsEnabled = false;
li.AllowFocusWhenDisabled = false;
li.AllowFocusOnInteraction = false;
li.IsHitTestVisible = false;
dataTemplate = string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
}
else
{
listItem.IsEnabled = true;
listItem.AllowFocusWhenDisabled = true;
listItem.AllowFocusOnInteraction = true;
listItem.IsHitTestVisible = true;
}
}
}
return dataTemplate;
}
}

View File

@@ -0,0 +1,56 @@
// 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 System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
/// <summary>
/// Tracks extension usage with extension name and invocation details.
/// Purpose: Identify popular vs. unused plugins and track extension performance.
/// </summary>
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalExtensionInvoked : EventBase, IEvent
{
/// <summary>
/// Gets or sets the unique identifier of the extension provider.
/// </summary>
public string ExtensionId { get; set; }
/// <summary>
/// Gets or sets the non-localized identifier of the command being invoked.
/// </summary>
public string CommandId { get; set; }
/// <summary>
/// Gets or sets the localized display name of the command being invoked.
/// </summary>
public string CommandName { get; set; }
/// <summary>
/// Gets or sets whether the command executed successfully.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets the execution time in milliseconds.
/// </summary>
public ulong ExecutionTimeMs { get; set; }
public CmdPalExtensionInvoked(string extensionId, string commandId, string commandName, bool success, ulong executionTimeMs)
{
EventName = "CmdPal_ExtensionInvoked";
ExtensionId = extensionId;
CommandId = commandId;
CommandName = commandName;
Success = success;
ExecutionTimeMs = executionTimeMs;
}
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -18,6 +18,7 @@ public class CmdPalInvokeResult : EventBase, IEvent
public CmdPalInvokeResult(CommandResultKind resultKind)
{
EventName = "CmdPal_InvokeResult";
ResultKind = resultKind.ToString();
}

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