Compare commits

...

18 Commits

Author SHA1 Message Date
Leilei Zhang
3e67ae1bd8 fix comments 2025-12-26 11:19:50 +08:00
Leilei Zhang
f58f4dc5ad update string 2025-12-25 13:48:54 +08:00
Leilei Zhang
e4dda98b6e remove wrong file 2025-12-25 12:31:20 +08:00
Leilei Zhang
b61231b3dc add localization 2025-12-25 12:16:30 +08:00
Leilei Zhang
aac813cb71 fix imageresizer UT 2025-12-15 14:34:16 +08:00
Leilei Zhang
02fa0daea5 Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/imagecli 2025-12-15 10:25:48 +08:00
Leilei Zhang (from Dev Box)
dcfb93b26d Add standard CLI support for Image Resizer 2025-12-15 09:53:08 +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
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
124 changed files with 7533 additions and 678 deletions

View File

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

View File

@@ -1854,6 +1854,8 @@ uitests
UITo
ULONGLONG
ums
UMax
UMin
uncompilable
UNCPRIORITY
UNDNAME

View File

@@ -131,6 +131,8 @@
"PowerToys.ImageResizer.exe",
"PowerToys.ImageResizer.dll",
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
"PowerToys.ImageResizerExt.dll",
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",

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",

View File

@@ -42,11 +42,6 @@
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<!-- Make angle-bracket includes external and turn off code analysis for them -->
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
<DisableAnalyzeExternal>true</DisableAnalyzeExternal>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
@@ -116,11 +111,13 @@
</PropertyGroup>
<!-- Debug/Release props -->
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<PropertyGroup Condition="'$(Configuration)'=='Debug'"
Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<PropertyGroup Condition="'$(Configuration)'=='Release'"
Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>

View File

@@ -7,8 +7,6 @@
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
<PackageVersion Include="boost" Version="1.87.0" TargetFramework="native" />
<PackageVersion Include="boost_regex-vc143" Version="1.87.0" TargetFramework="native" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
@@ -72,12 +70,10 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
@@ -116,7 +112,6 @@
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageVersion Include="System.Management" Version="9.0.10" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />

View File

@@ -442,6 +442,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/imageresizer/Tests/">
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">

View File

@@ -0,0 +1,93 @@
# CLI Conventions
This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
## Library
Use the **System.CommandLine** library for CLI argument parsing. This is already defined in `Directory.Packages.props`:
```xml
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
```
Add the reference to your project:
```xml
<PackageReference Include="System.CommandLine" />
```
## Option Naming and Definition
- Use `--kebab-case` for long form (e.g., `--shrink-only`).
- Use single `-x` for short form (e.g., `-s`, `-w`).
- Define aliases as static readonly arrays: `["--silent", "-s"]`.
- Create options using `Option<T>` with descriptive help text.
- Add validators for options that require range or format checking.
## RootCommand Setup
- Create a `RootCommand` with a brief description.
- Add all options and arguments to the command.
## Parsing
- Use `Parser(rootCommand).Parse(args)` to parse CLI arguments.
- Extract option values using `parseResult.GetValueForOption()`.
- Note: Use `Parser` directly; `RootCommand.Parse()` may not be available with the pinned System.CommandLine version.
### Parse/Validation Errors
- On parse/validation errors, print error messages and usage, then exit with non-zero code.
## Examples
Reference implementations:
- Awake: `src/modules/Awake/Awake/Program.cs`
- ImageResizer: `src/modules/imageresizer/ui/Cli/`
## Help Output
- Provide a `PrintUsage()` method for custom help formatting if needed.
## Best Practices
1. **Consistency**: Follow existing module patterns.
2. **Documentation**: Always provide help text for each option.
3. **Validation**: Validate input and provide clear error messages.
4. **Atomicity**: Make one logical change per PR; avoid drive-by refactors.
5. **Build/Test Discipline**: Build and test synchronously, one terminal per operation.
6. **Style**: Follow repo analyzers (`.editorconfig`, StyleCop) and formatting rules.
## Logging Requirements
- Use `ManagedCommon.Logger` for consistent logging.
- Initialize logging early in `Main()`.
- Use dual output (console + log file) for errors and warnings to ensure visibility.
- Reference: `src/modules/imageresizer/ui/Cli/CliLogger.cs`
## Error Handling
### Exit Codes
- `0`: Success
- `1`: General error (parsing, validation, runtime)
- `2`: Invalid arguments (optional)
### Exception Handling
- Always wrap `Main()` in try-catch for unhandled exceptions.
- Log exceptions before exiting with non-zero code.
- Display user-friendly error messages to stderr.
- Preserve detailed stack traces in log files only.
## Testing Requirements
- Include tests for argument parsing, validation, and edge cases.
- Place CLI tests in module-specific test projects (e.g., `src/modules/[module]/tests/*CliTests.cs`).
## Signing and Deployment
- CLI executables are signed automatically in CI/CD.
- **New CLI tools**: Add your executable and dll to `.pipelines/ESRPSigning_core.json` in the signing list.
- CLI executables are deployed alongside their parent module (e.g., `C:\Program Files\PowerToys\modules\[ModuleName]\`).
- Use self-contained deployment (import `Common.SelfContained.props`).

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

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

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

@@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="NuGet">
<!-- Tell NuGet this is PackageReference style -->
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<!-- Tell NuGet we're a native project -->
<NuGetTargetMoniker>native,Version=v0.0</NuGetTargetMoniker>
<!-- Tell NuGet we target Windows (use your existing WindowsTargetPlatformVersion) -->
<NuGetTargetPlatformIdentifier>Windows</NuGetTargetPlatformIdentifier>
<NuGetTargetPlatformVersion>$(WindowsTargetPlatformVersion)</NuGetTargetPlatformVersion>
</PropertyGroup>
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
@@ -33,11 +31,6 @@
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -45,6 +38,7 @@
<DesktopCompatible>true</DesktopCompatible>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets">
@@ -124,6 +118,9 @@
<WarnAsError>true</WarnAsError>
</Midl>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
@@ -145,5 +142,42 @@
<ResourceCompile Include="PowerToys.MeasureToolCore.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
</packages>

View File

@@ -73,13 +73,6 @@
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<BuildProject>true</BuildProject>
</ProjectReference>
<CsWinRTInputs Include="$(OutputPath)\PowerToys.MeasureToolCore.winmd" />
<None Include="$(OutputPath)\PowerToys.MeasureToolCore.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<ProjectReference Include="..\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -1,16 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="NuGet">
<!-- Tell NuGet this is PackageReference style -->
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<!-- Tell NuGet we're a native project -->
<NuGetTargetMoniker>native,Version=v0.0</NuGetTargetMoniker>
<!-- Tell NuGet we target Windows (use your existing WindowsTargetPlatformVersion) -->
<NuGetTargetPlatformIdentifier>Windows</NuGetTargetPlatformIdentifier>
<NuGetTargetPlatformVersion>$(WindowsTargetPlatformVersion)</NuGetTargetPlatformVersion>
</PropertyGroup>
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{e94fd11c-0591-456f-899f-efc0ca548336}</ProjectGuid>
@@ -23,12 +20,9 @@
<WindowsAppSdkBootstrapInitialize>false</WindowsAppSdkBootstrapInitialize>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSDKVerifyTransitiveDependencies>false</WindowsAppSDKVerifyTransitiveDependencies>
<!-- Force NuGet to treat this project strictly as packages.config style -->
<RestoreProjectStyle>packages.config</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true"/>
<PackageReference Include="Microsoft.WindowsAppSDK.Foundation" GeneratePathProperty="true"/>
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true"/>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -133,18 +127,18 @@
<ItemGroup>
<ResourceCompile Include="FindMyMouse.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<!-- Deduplicate WindowsAppRuntimeAutoInitializer.cpp (added twice via transitive imports causing LNK4042). Remove all then add exactly once. -->
<Target Name="FixWinAppSDKAutoInitializer" BeforeTargets="ClCompile" AfterTargets="WindowsAppRuntimeAutoInitializer">
<ItemGroup>
<!-- Remove ALL injected versions of the file -->
<ClCompile Remove="@(ClCompile)" Condition="'%(Filename)' == 'WindowsAppRuntimeAutoInitializer'" />
<!-- Add ONE copy back manually -->
<ClCompile Include="$(PkgMicrosoft_WindowsAppSDK_Foundation)\include\WindowsAppRuntimeAutoInitializer.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemGroup>
</Target>
<ItemGroup Condition="'$(PkgMicrosoft_WindowsAppSDK)'!=''">
<!-- Remove any transitive inclusion first -->
<ClCompile Remove="$(PkgMicrosoft_WindowsAppSDK)\include\WindowsAppRuntimeAutoInitializer.cpp" />
<!-- Re-add once, but disable PCH because the SDK file doesn't include our pch.h -->
<ClCompile Include="$(PkgMicrosoft_WindowsAppSDK)\include\WindowsAppRuntimeAutoInitializer.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<Target Name="RemoveManagedWebView2CoreFromNativeOutDir" AfterTargets="Build">
<ItemGroup>
<_ToDelete Include="$(OutDir)Microsoft.Web.WebView2.Core.dll" />
@@ -154,4 +148,38 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.Windows.CppWinRT.2.0.240111.5\\build\\native\\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.Windows.CppWinRT.2.0.240111.5\\build\\native\\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.Windows.CppWinRT.2.0.240111.5\\build\\native\\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.Windows.CppWinRT.2.0.240111.5\\build\\native\\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.Web.WebView2.1.0.2903.40\\build\\native\\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.Web.WebView2.1.0.2903.40\\build\\native\\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Base.1.8.250831001\\build\\native\\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Base.1.8.250831001\\build\\native\\Microsoft.WindowsAppSDK.Base.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Base.1.8.250831001\\build\\native\\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Base.1.8.250831001\\build\\native\\Microsoft.WindowsAppSDK.Base.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\\build\\native\\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\\build\\native\\Microsoft.WindowsAppSDK.Foundation.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\\build\\native\\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\\build\\native\\Microsoft.WindowsAppSDK.Foundation.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\\build\\native\\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\\build\\native\\Microsoft.WindowsAppSDK.WinUI.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\\build\\native\\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\\build\\native\\Microsoft.WindowsAppSDK.WinUI.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.Runtime.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\\build\\native\\Microsoft.WindowsAppSDK.Runtime.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\\build\\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\\build\\Microsoft.WindowsAppSDK.DWrite.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\\build\\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\\build\\Microsoft.WindowsAppSDK.DWrite.targets'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\\build\\native\\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\\build\\native\\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
<Error Condition="!Exists('..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\\build\\native\\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\\..\\..\\..\\packages\\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\\build\\native\\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
</packages>

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

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

@@ -6,12 +6,12 @@
<!-- For MVVM Toolkit Partial Properties/AOT support -->
<LangVersion>preview</LangVersion>
<OutputPath>..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\</OutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>$(RootNamespace).pri</ProjectPriFileName>
<!-- Disable SA1313 for Primary Constructor fields conflict https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors -->
<NoWarn>SA1313;</NoWarn>
@@ -42,5 +42,5 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

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

@@ -24,7 +24,7 @@
<ItemGroup>
<!-- Images -->
<Content Include=".\Assets\$(CmdPalAssetSuffix)\**\*">
<Content Include="$(SolutionDir)\src\modules\cmdpal\Microsoft.CmdPal.UI\Assets\$(CmdPalAssetSuffix)\**\*">
<DeploymentContent>true</DeploymentContent>
<Link>Assets\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
@@ -35,10 +35,14 @@
<!-- In the future, when we actually want to support "preview" and "canary",
add a Package-Pre.appxmanifest, etc. -->
<AppxManifest Include="Package.appxmanifest" Condition="'$(CommandPaletteBranding)'=='Release'" />
<AppxManifest Include="Package.appxmanifest" Condition="'$(CommandPaletteBranding)'=='Preview'" />
<AppxManifest Include="Package.appxmanifest" Condition="'$(CommandPaletteBranding)'=='Canary'" />
<AppxManifest Include="Package-Dev.appxmanifest" Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'" />
<AppxManifest Include="Package.appxmanifest"
Condition="'$(CommandPaletteBranding)'=='Release'" />
<AppxManifest Include="Package.appxmanifest"
Condition="'$(CommandPaletteBranding)'=='Preview'" />
<AppxManifest Include="Package.appxmanifest"
Condition="'$(CommandPaletteBranding)'=='Canary'" />
<AppxManifest Include="Package-Dev.appxmanifest"
Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'" />
</ItemGroup>
</Project>

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<PropertyGroup>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<!-- Reset this because the Versioning task might have overwritten it before it knew about OutDir -->
<AppxPackageDir>$(OutputPath)\AppPackages\</AppxPackageDir>
</PropertyGroup>

View File

@@ -368,32 +368,69 @@ internal sealed partial class BlurImageControl : Control
{
try
{
if (imageSource is Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
{
_imageBrush ??= _compositor?.CreateSurfaceBrush();
if (_imageBrush is null)
{
return;
}
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
loadedSurface.LoadCompleted += (_, _) =>
{
if (_imageBrush is not null)
{
_imageBrush.Surface = loadedSurface;
_imageBrush.Stretch = ConvertStretch(ImageStretch);
_imageBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
}
};
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
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)

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,416 @@
// 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;
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 StretchChild StretchChild
{
get { return (StretchChild)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(StretchChild),
typeof(WrapPanel),
new PropertyMetadata(StretchChild.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 == StretchChild.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,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

@@ -28,6 +28,8 @@
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
@@ -90,6 +92,8 @@
</Style>
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
@@ -168,8 +172,17 @@
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource MediumGridItemViewModelTemplate}"
Section="{StaticResource ListSectionViewModelTemplate}"
Separator="{StaticResource ListSeparatorViewModelTemplate}"
Small="{StaticResource SmallGridItemViewModelTemplate}" />
<cmdpalUI:ListItemTemplateSelector
x:Key="ListItemTemplateSelector"
x:DataType="coreViewModels:ListItemViewModel"
ListItem="{StaticResource ListItemViewModelTemplate}"
Section="{StaticResource ListSectionViewModelTemplate}"
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
<cmdpalUI:GridItemContainerStyleSelector
x:Key="GridItemContainerStyleSelector"
Gallery="{StaticResource GalleryGridViewItemStyle}"
@@ -241,12 +254,46 @@
</Grid>
</DataTemplate>
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.Column="1"
Height="1"
Margin="0,2,0,2"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="ListSectionViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<Grid
Margin="0"
VerticalAlignment="Center"
cpcontrols:WrapPanel.IsFullLine="True"
ColumnSpacing="8"
IsTabStop="False"
IsTapEnabled="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Foreground="{ThemeResource TextFillColorDisabled}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind Section}" />
<Rectangle
Grid.Column="1"
Height="1"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Grid>
</DataTemplate>
<!-- Grid item templates for visual grid representation -->
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel
Width="60"
Height="60"
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
@@ -265,7 +312,6 @@
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
</StackPanel>
</DataTemplate>
@@ -393,13 +439,16 @@
<ListView
x:Name="ItemsList"
Padding="0,2,0,0"
CanDragItems="True"
ContextCanceled="Items_OnContextCanceled"
ContextRequested="Items_OnContextRequested"
DoubleTapped="Items_DoubleTapped"
DragItemsCompleted="Items_DragItemsCompleted"
DragItemsStarting="Items_DragItemsStarting"
IsDoubleTapEnabled="True"
IsItemClickEnabled="True"
ItemClick="Items_ItemClick"
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
RightTapped="Items_RightTapped"
SelectionChanged="Items_SelectionChanged">
@@ -411,10 +460,13 @@
<controls:Case Value="True">
<GridView
x:Name="ItemsGrid"
Padding="8"
Padding="16,0"
CanDragItems="True"
ContextCanceled="Items_OnContextCanceled"
ContextRequested="Items_OnContextRequested"
DoubleTapped="Items_DoubleTapped"
DragItemsCompleted="Items_DragItemsCompleted"
DragItemsStarting="Items_DragItemsStarting"
IsDoubleTapEnabled="True"
IsItemClickEnabled="True"
ItemClick="Items_ItemClick"
@@ -423,10 +475,14 @@
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
RightTapped="Items_RightTapped"
SelectionChanged="Items_SelectionChanged">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemContainerTransitions>
<TransitionCollection />
</GridView.ItemContainerTransitions>
<GridView.ItemContainerStyle />
</GridView>
</controls:Case>
</controls:SwitchPresenter>

View File

@@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.System;
@@ -76,12 +77,18 @@ public sealed partial class ListPage : Page,
ViewModel = listViewModel;
if (e.NavigationMode == NavigationMode.Back
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
if (e.NavigationMode == NavigationMode.Back)
{
// Upon navigating _back_ to this page, immediately select the
// first item in the list
ItemView.SelectedIndex = 0;
// Must dispatch the selection to run at a lower priority; otherwise, GetFirstSelectableIndex
// may return an incorrect index because item containers are not yet rendered.
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
var firstUsefulIndex = GetFirstSelectableIndex();
if (firstUsefulIndex != -1)
{
ItemView.SelectedIndex = firstUsefulIndex;
}
});
}
// RegisterAll isn't AOT compatible
@@ -128,6 +135,29 @@ public sealed partial class ListPage : Page,
GC.Collect();
}
/// <summary>
/// Finds the index of the first item in the list that is not a separator.
/// Returns -1 if the list is empty or only contains separators.
/// </summary>
private int GetFirstSelectableIndex()
{
var items = ItemView.Items;
if (items is null || items.Count == 0)
{
return -1;
}
for (var i = 0; i < items.Count; i++)
{
if (!IsSeparator(items[i]))
{
return i;
}
}
return -1;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
private void Items_ItemClick(object sender, ItemClickEventArgs e)
{
@@ -183,19 +213,33 @@ public sealed partial class ListPage : Page,
// here, then in Page_ItemsUpdated trying to select that cached item if
// it's in the list (otherwise, clear the cache), but that seems
// aggressively BODGY for something that mostly just works today.
if (ItemView.SelectedItem is not null)
if (ItemView.SelectedItem is not null && !IsSeparator(ItemView.SelectedItem))
{
ItemView.ScrollIntoView(ItemView.SelectedItem);
var items = ItemView.Items;
var firstUsefulIndex = GetFirstSelectableIndex();
var shouldScroll = false;
if (e.RemovedItems.Count > 0)
{
shouldScroll = true;
}
else if (ItemView.SelectedIndex > firstUsefulIndex)
{
shouldScroll = true;
}
if (shouldScroll)
{
ItemView.ScrollIntoView(ItemView.SelectedItem);
}
// Automation notification for screen readers
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
if (listViewPeer is not null && li is not null)
{
var notificationText = li.Title;
UIHelper.AnnounceActionForAccessibility(
ItemsList,
notificationText,
li.Title,
"CommandPaletteSelectedItemChanged");
}
}
@@ -271,14 +315,7 @@ public sealed partial class ListPage : Page,
else
{
// For list views, use simple linear navigation
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
{
ItemView.SelectedIndex++;
}
else
{
ItemView.SelectedIndex = 0;
}
NavigateDown();
}
}
@@ -291,15 +328,7 @@ public sealed partial class ListPage : Page,
}
else
{
// For list views, use simple linear navigation
if (ItemView.SelectedIndex > 0)
{
ItemView.SelectedIndex--;
}
else
{
ItemView.SelectedIndex = ItemView.Items.Count - 1;
}
NavigateUp();
}
}
@@ -366,7 +395,10 @@ public sealed partial class ListPage : Page,
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
{
ItemView.SelectedIndex = indexes.Value.TargetIndex;
ItemView.ScrollIntoView(ItemView.SelectedItem);
if (ItemView.SelectedItem is not null)
{
ItemView.ScrollIntoView(ItemView.SelectedItem);
}
}
}
@@ -381,7 +413,10 @@ public sealed partial class ListPage : Page,
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
{
ItemView.SelectedIndex = indexes.Value.TargetIndex;
ItemView.ScrollIntoView(ItemView.SelectedItem);
if (ItemView.SelectedItem is not null)
{
ItemView.ScrollIntoView(ItemView.SelectedItem);
}
}
}
@@ -524,17 +559,65 @@ public sealed partial class ListPage : Page,
// ItemView_SelectionChanged again to give us another chance to change
// the selection from null -> something. Better to just update the
// selection once, at the end of all the updating.
if (ItemView.SelectedItem is null)
// The selection logic must be deferred to the DispatcherQueue
// to ensure the UI has processed the updated ItemsSource binding,
// preventing ItemView.Items from appearing empty/null immediately after update.
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
ItemView.SelectedIndex = 0;
}
var items = ItemView.Items;
// Always reset the selected item when the top-level list page changes
// its items
if (!sender.IsNested)
{
ItemView.SelectedIndex = 0;
}
// If the list is null or empty, clears the selection and return
if (items is null || items.Count == 0)
{
ItemView.SelectedIndex = -1;
return;
}
// Finds the first item that is not a separator
var firstUsefulIndex = GetFirstSelectableIndex();
// If there is only separators in the list, don't select anything.
if (firstUsefulIndex == -1)
{
ItemView.SelectedIndex = -1;
return;
}
var shouldUpdateSelection = false;
// If it's a top level list update we force the reset to the top useful item
if (!sender.IsNested)
{
shouldUpdateSelection = true;
}
// No current selection or current selection is null
else if (ItemView.SelectedItem is null)
{
shouldUpdateSelection = true;
}
// The current selected item is a separator
else if (IsSeparator(ItemView.SelectedItem))
{
shouldUpdateSelection = true;
}
// The selected item does not exist in the new list
else if (!items.Contains(ItemView.SelectedItem))
{
shouldUpdateSelection = true;
}
if (shouldUpdateSelection)
{
if (firstUsefulIndex != -1)
{
ItemView.SelectedIndex = firstUsefulIndex;
}
}
});
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -604,6 +687,11 @@ public sealed partial class ListPage : Page,
continue;
}
if (IsSeparator(ItemView.Items[i]))
{
continue;
}
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
{
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
@@ -764,6 +852,185 @@ public sealed partial class ListPage : Page,
}
}
/// <summary>
/// Code stealed from <see cref="Controls.ContextMenu.NavigateUp"/>
/// </summary>
private void NavigateUp()
{
var newIndex = ItemView.SelectedIndex;
if (ItemView.SelectedIndex > 0)
{
newIndex--;
while (
newIndex >= 0 &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex--;
}
if (newIndex < 0)
{
newIndex = ItemView.Items.Count - 1;
while (
newIndex >= 0 &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex--;
}
}
}
else
{
newIndex = ItemView.Items.Count - 1;
}
ItemView.SelectedIndex = newIndex;
}
private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
try
{
if (e.Items.FirstOrDefault() is not ListItemViewModel item || item.DataPackage is null)
{
e.Cancel = true;
return;
}
// copy properties
foreach (var (key, value) in item.DataPackage.Properties)
{
try
{
e.Data.Properties[key] = value;
}
catch (Exception)
{
// noop - skip any properties that fail
}
}
// setup e.Data formats as deferred renderers to read from the item's DataPackage
foreach (var format in item.DataPackage.AvailableFormats)
{
try
{
e.Data.SetDataProvider(format, request => DelayRenderer(request, item, format));
}
catch (Exception)
{
// noop - skip any formats that fail
}
}
WeakReferenceMessenger.Default.Send(new DragStartedMessage());
}
catch (Exception ex)
{
WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
Logger.LogError("Failed to start dragging an item", ex);
}
}
private static void DelayRenderer(DataProviderRequest request, ListItemViewModel item, string format)
{
var deferral = request.GetDeferral();
try
{
item.DataPackage?.GetDataAsync(format)
.AsTask()
.ContinueWith(dataTask =>
{
try
{
if (dataTask.IsCompletedSuccessfully)
{
request.SetData(dataTask.Result);
}
else if (dataTask.IsFaulted && dataTask.Exception is not null)
{
Logger.LogError($"Failed to get data for format '{format}' during drag-and-drop", dataTask.Exception);
}
}
finally
{
deferral.Complete();
}
});
}
catch (Exception ex)
{
Logger.LogError($"Failed to set data for format '{format}' during drag-and-drop", ex);
deferral.Complete();
}
}
private void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
}
/// <summary>
/// Code stealed from <see cref="Controls.ContextMenu.NavigateDown"/>
/// </summary>
private void NavigateDown()
{
var newIndex = ItemView.SelectedIndex;
if (ItemView.SelectedIndex == ItemView.Items.Count - 1)
{
newIndex = 0;
while (
newIndex < ItemView.Items.Count &&
IsSeparator(ItemView.Items[newIndex]))
{
newIndex++;
}
if (newIndex >= ItemView.Items.Count)
{
return;
}
}
else
{
newIndex++;
while (
newIndex < ItemView.Items.Count &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex++;
}
if (newIndex >= ItemView.Items.Count)
{
newIndex = 0;
while (
newIndex < ItemView.Items.Count &&
IsSeparator(ItemView.Items[newIndex]) &&
newIndex != ItemView.SelectedIndex)
{
newIndex++;
}
}
}
ItemView.SelectedIndex = newIndex;
}
/// <summary>
/// Code stealed from <see cref="Controls.ContextMenu.IsSeparator(object)"/>
/// </summary>
private bool IsSeparator(object? item) => item is ListItemViewModel li && li.IsSectionOrSeparator;
private enum InputSource
{
None,

View File

@@ -52,6 +52,8 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>,
IRecipient<DragStartedMessage>,
IRecipient<DragCompletedMessage>,
IDisposable
{
private const int DefaultWidth = 800;
@@ -79,6 +81,8 @@ public sealed partial class MainWindow : WindowEx,
private WindowPosition _currentWindowPosition = new();
private bool _preventHideWhenDeactivated;
private MainWindowViewModel ViewModel { get; }
public MainWindow()
@@ -119,6 +123,8 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
// Hide our titlebar.
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
@@ -751,6 +757,12 @@ public sealed partial class MainWindow : WindowEx,
return;
}
// We're doing something that requires us to lose focus, but we don't want to hide the window
if (_preventHideWhenDeactivated)
{
return;
}
// This will DWM cloak our window:
HideWindow();
@@ -1027,4 +1039,44 @@ public sealed partial class MainWindow : WindowEx,
_windowThemeSynchronizer.Dispose();
DisposeAcrylic();
}
public void Receive(DragStartedMessage message)
{
_preventHideWhenDeactivated = true;
}
public void Receive(DragCompletedMessage message)
{
_preventHideWhenDeactivated = false;
Task.Delay(200).ContinueWith(_ =>
{
DispatcherQueue.TryEnqueue(StealForeground);
});
}
private unsafe void StealForeground()
{
var foregroundWindow = PInvoke.GetForegroundWindow();
if (foregroundWindow == _hwnd)
{
return;
}
// This is bad, evil, and I'll have to forgo today's dinner dessert to punish myself
// for writing this. But there's no way to make this work without it.
// If the window is not reactivated, the UX breaks down: a deactivated window has to
// be activated and then deactivated again to hide.
var currentThreadId = PInvoke.GetCurrentThreadId();
var foregroundThreadId = PInvoke.GetWindowThreadProcessId(foregroundWindow, null);
if (foregroundThreadId != currentThreadId)
{
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, true);
PInvoke.SetForegroundWindow(_hwnd);
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, false);
}
else
{
PInvoke.SetForegroundWindow(_hwnd);
}
}
}

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.Messages;
public record DragCompletedMessage;

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.Messages;
public record DragStartedMessage;

View File

@@ -15,7 +15,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<LangVersion>preview</LangVersion>
<LangVersion>preview</LangVersion>
<Version>$(CmdPalVersion)</Version>
@@ -23,14 +23,13 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<UseWinRT>true</UseWinRT>
</PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
<!--<PropertyGroup>
<!--<PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT>
<GeneratePackageLocally>true</GeneratePackageLocally>
</PropertyGroup>-->
</PropertyGroup>-->
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
<SelfContained>true</SelfContained>
@@ -46,7 +45,7 @@
</PropertyGroup>
<PropertyGroup>
<!-- This lets us actually reference types from Microsoft.Terminal.UI and CmdPalKeyboardService -->
<!-- This lets us actually reference types from Microsoft.Terminal.UI -->
<CsWinRTIncludes>Microsoft.Terminal.UI;CmdPalKeyboardService</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
@@ -102,7 +101,7 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -148,16 +147,12 @@
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj" />
<ProjectReference Include="$(ProjectDir)\..\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj">
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
<BuildProject>True</BuildProject>
<ProjectReference Include="..\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj">
<ReferenceOutputAssembly>True</ReferenceOutputAssembly>
<Private>True</Private>
<CopyLocalSatelliteAssemblies>True</CopyLocalSatelliteAssemblies>
</ProjectReference>
<!-- WinRT metadata reference -->
<CsWinRTInputs Include="$(OutputPath)\Microsoft.Terminal.UI.winmd" />
<!-- Native implementation DLL -->
<None Include="$(OutputPath)\Microsoft.Terminal.UI.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<ProjectReference Include="..\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj">
<ReferenceOutputAssembly>True</ReferenceOutputAssembly>
<Private>True</Private>

View File

@@ -63,4 +63,7 @@ CreateWindowEx
WNDCLASSEXW
RegisterClassEx
GetStockObject
GetModuleHandle
GetModuleHandle
GetWindowThreadProcessId
AttachThreadInput

View File

@@ -26,6 +26,7 @@
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<cmdpalUI:DetailsSizeToGridLengthConverter x:Key="SizeToWidthConverter" />
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
<cmdpalUI:DetailsDataTemplateSelector
@@ -370,7 +371,7 @@
<Grid x:Name="ContentGrid" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="{x:Bind ViewModel.Details.Size, Mode=OneWay, Converter={StaticResource SizeToWidthConverter}}" />
<ColumnDefinition x:Name="DetailsColumn" Width="Auto" />
</Grid.ColumnDefinitions>

View File

@@ -105,6 +105,7 @@ public sealed partial class SettingsWindow : WindowEx,
"Extensions" => typeof(ExtensionsPage),
_ => null,
};
if (pageType is not null)
{
NavFrame.Navigate(pageType);

View File

@@ -12,9 +12,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
{
constexpr UChar32 fluentIconsPrivateUseAreaStart = 0xE700;
constexpr UChar32 fluentIconsPrivateUseAreaEnd = 0xF8FF;
return cp >= fluentIconsPrivateUseAreaStart && cp <= fluentIconsPrivateUseAreaEnd;
static constexpr UChar32 _fluentIconsPrivateUseAreaStart = 0xE700;
static constexpr UChar32 _fluentIconsPrivateUseAreaEnd = 0xF8FF;
return cp >= _fluentIconsPrivateUseAreaStart && cp <= _fluentIconsPrivateUseAreaEnd;
}
// Determine if the given text (as a sequence of UChar code units) is emoji

View File

@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="NuGet">
<!-- Tell NuGet this is PackageReference style -->
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<!-- Tell NuGet we're a native project -->
<NuGetTargetMoniker>native,Version=v0.0</NuGetTargetMoniker>
<!-- Tell NuGet we target Windows (use your existing WindowsTargetPlatformVersion) -->
<NuGetTargetPlatformIdentifier>Windows</NuGetTargetPlatformIdentifier>
<NuGetTargetPlatformVersion>$(WindowsTargetPlatformVersion)</NuGetTargetPlatformVersion>
</PropertyGroup>
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup>
<PathToRoot>..\..\..\..\</PathToRoot>
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.8.250907003</WasdkNuget>
</PropertyGroup>
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
@@ -27,11 +28,6 @@
<WindowsTargetPlatformVersion Condition=" '$(WindowsTargetPlatformVersion)' == '' ">10.0.26100.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.19041.0</WindowsTargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
@@ -51,6 +47,10 @@
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutDir>
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
@@ -200,11 +200,43 @@
<Midl Include="FontIconGlyphClassifier.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="Microsoft.Terminal.UI.def" />
</ItemGroup>
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutDir>
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
<ImportGroup Label="ExtensionTargets">
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(MSBuildThisFileDirectory)..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', 'Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The packages.config acts as the global version for all of the NuGet packages contained within. -->
<packages>
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
</packages>

View File

@@ -12,6 +12,8 @@ using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using WinRT;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
@@ -62,6 +64,8 @@ internal sealed partial class ClipboardListItem : ListItem
RequestedShortcut = KeyChords.DeleteEntry,
};
DataPackageView = _item.Item.Content;
if (item.IsImage)
{
Title = "Image";

View File

@@ -2,14 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CmdPal.Core.Common.Commands;
using Microsoft.CmdPal.Ext.Indexer.Helpers;
using Microsoft.CmdPal.Ext.Indexer.Pages;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation.Metadata;
using FileAttributes = System.IO.FileAttributes;
namespace Microsoft.CmdPal.Ext.Indexer.Data;
@@ -36,6 +39,8 @@ internal sealed partial class IndexerListItem : ListItem
Title = indexerItem.FileName;
Subtitle = indexerItem.FullPath;
DataPackage = DataPackageHelper.CreateDataPackageForPath(this, FilePath);
var commands = FileCommands(indexerItem.FullPath, browseByDefault);
if (commands.Any())
{

View File

@@ -7,6 +7,7 @@ using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Helpers;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
@@ -42,6 +43,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
Subtitle = string.Empty;
Icon = null;
MoreCommands = null;
DataPackage = null;
return;
}
@@ -53,6 +55,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
Subtitle = string.Empty;
Icon = null;
MoreCommands = null;
DataPackage = null;
return;
}
@@ -67,6 +70,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
Subtitle = item.FileName;
Title = item.FullPath;
Icon = listItemForUs.Icon;
DataPackage = DataPackageHelper.CreateDataPackageForPath(listItemForUs, item.FullPath);
try
{
@@ -92,13 +96,15 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
_searchEngine.Query(query, _queryCookie);
var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _);
if (results.Count == 0 || ((results[0] as IndexerListItem) is null))
if (results.Count == 0 || (results[0] is not IndexerListItem indexerListItem))
{
// Exit 2: We searched for the file, and found nothing. Oh well.
// Hide ourselves.
Title = string.Empty;
Subtitle = string.Empty;
Command = new NoOpCommand();
MoreCommands = null;
DataPackage = null;
return;
}
@@ -106,11 +112,12 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
{
// Exit 3: We searched for the file, and found exactly one thing. Awesome!
// Return it.
Title = results[0].Title;
Subtitle = results[0].Subtitle;
Icon = results[0].Icon;
Command = results[0].Command;
MoreCommands = results[0].MoreCommands;
Title = indexerListItem.Title;
Subtitle = indexerListItem.Subtitle;
Icon = indexerListItem.Icon;
Command = indexerListItem.Command;
MoreCommands = indexerListItem.MoreCommands;
DataPackage = DataPackageHelper.CreateDataPackageForPath(indexerListItem, indexerListItem.FilePath);
return;
}
@@ -121,6 +128,8 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query);
Icon = Icons.FileExplorerIcon;
Command = indexerPage;
MoreCommands = null;
DataPackage = null;
return;
}
@@ -131,6 +140,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
Icon = null;
Command = new NoOpCommand();
MoreCommands = null;
DataPackage = null;
}
}
}

View File

@@ -0,0 +1,64 @@
// 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.IO;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using File = System.IO.File;
namespace Microsoft.CmdPal.Ext.Indexer.Helpers;
internal static class DataPackageHelper
{
public static DataPackage CreateDataPackageForPath(ICommandItem listItem, string path)
{
if (string.IsNullOrEmpty(path))
{
return null;
}
var dataPackage = new DataPackage();
dataPackage.SetText(path);
_ = dataPackage.TrySetStorageItemsAsync(path);
dataPackage.Properties.Title = listItem.Title;
dataPackage.Properties.Description = listItem.Subtitle;
dataPackage.RequestedOperation = DataPackageOperation.Copy;
return dataPackage;
}
public static async Task<bool> TrySetStorageItemsAsync(this DataPackage dataPackage, string filePath)
{
try
{
if (File.Exists(filePath))
{
var file = await StorageFile.GetFileFromPathAsync(filePath);
dataPackage.SetStorageItems([file]);
return true;
}
if (Directory.Exists(filePath))
{
var folder = await StorageFolder.GetFolderFromPathAsync(filePath);
dataPackage.SetStorageItems([folder]);
return true;
}
// nothing there
return false;
}
catch (UnauthorizedAccessException)
{
// Access denied skip or report, but don't crash
return false;
}
catch (Exception)
{
return false;
}
}
}

View File

@@ -5,6 +5,7 @@
using System.Collections.Generic;
using Microsoft.CmdPal.Core.Common.Commands;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Helpers;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -28,6 +29,9 @@ internal sealed partial class ExploreListItem : ListItem
Title = indexerItem.FileName;
Subtitle = indexerItem.FullPath;
DataPackage = DataPackageHelper.CreateDataPackageForPath(this, FilePath);
List<CommandContextItem> context = [];
if (indexerItem.IsDirectory())
{

View File

@@ -5,7 +5,6 @@
using System;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml;
namespace SamplePagesExtension;
@@ -23,14 +22,34 @@ internal sealed partial class SampleListPageWithDetails : ListPage
return [
new ListItem(new NoOpCommand())
{
Title = "This page demonstrates Details on ListItems",
Title = "Details on ListItems (Small)",
Details = new Details()
{
Title = "List Item 1",
Title = "This item has default details size",
Body = "Each of these items can have a `Body` formatted with **Markdown**",
},
},
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,
},
},
new ListItem(new NoOpCommand())
{
Title = "Details on ListItems (Large)",
Details = new Details()
{
Title = "This item has large details size",
Body = "Each of these items can have a `Body` formatted with **Markdown**",
Size = ContentSize.Large,
},
},
new ListItem(new NoOpCommand())
{
Title = "This one has a subtitle too",
Subtitle = "Example Subtitle",
@@ -70,11 +89,13 @@ internal sealed partial class SampleListPageWithDetails : ListPage
new ListItem(new NoOpCommand())
{
Title = "This one has metadata",
Subtitle = "And Large Details panel",
Tags = [],
Details = new Details()
{
Title = "Metadata Example",
Body = "Each of the sections below is some sample metadata",
Size = ContentSize.Large,
Metadata = [
new DetailsElement()
{

View File

@@ -0,0 +1,114 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension.Pages.SectionsPages;
internal sealed partial class SampleListPageWithSections : ListPage
{
public SampleListPageWithSections()
{
Icon = new IconInfo("\uE7C5");
Name = "Sample Gallery List Page";
}
public SampleListPageWithSections(IGridProperties gridProperties)
{
Icon = new IconInfo("\uE7C5");
Name = "Sample Gallery List Page";
GridProperties = gridProperties;
}
public override IListItem[] GetItems()
{
var sectionList = new Section("This is a section list", [
new ListItem(new NoOpCommand())
{
Title = "Sample Title",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
},
]);
var anotherSectionList = new Section("This is another section list", [
new ListItem(new NoOpCommand())
{
Title = "Another Title",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
},
new ListItem(new NoOpCommand())
{
Title = "More Titles",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Stop With The Titles",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
]);
var yesTheresAnother = new Section("There's another", [
new ListItem(new NoOpCommand())
{
Title = "Sample Title",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Another Title",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "More Titles",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Stop With The Titles",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Another Title",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
},
new ListItem(new NoOpCommand())
{
Title = "More Titles",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Stop With The Titles",
Subtitle = "I don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
]);
return [
..sectionList,
..anotherSectionList,
new Separator(),
new ListItem(new NoOpCommand())
{
Title = "Separators also work",
Subtitle = "But I still don't do anything",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
..yesTheresAnother
];
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using SamplePagesExtension.Pages.SectionsPages;
namespace SamplePagesExtension.Pages;
internal sealed partial class SectionsIndexPage : ListPage
{
public SectionsIndexPage()
{
Name = "Sections Index Page";
Icon = new IconInfo("\uF168");
}
public override IListItem[] GetItems()
{
return [
new ListItem(new SampleListPageWithSections())
{
Title = "A list page with sections",
},
new ListItem(new SampleListPageWithSections(new SmallGridLayout()))
{
Title = "A small grid page with sections",
},
new ListItem(new SampleListPageWithSections(new MediumGridLayout()))
{
Title = "A medium grid page with sections",
},
new ListItem(new SampleListPageWithSections(new GalleryGridLayout()))
{
Title = "A Gallery grid page with sections",
},
];
}
}

View File

@@ -0,0 +1,254 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
namespace SamplePagesExtension;
internal sealed partial class SampleDataTransferPage : ListPage
{
private readonly IListItem[] _items;
public SampleDataTransferPage()
{
var dataPackageWithText = CreateDataPackageWithText();
var dataPackageWithDelayedText = CreateDataPackageWithDelayedText();
var dataPackageWithImage = CreateDataPackageWithImage();
_items =
[
new ListItem(new NoOpCommand())
{
Title = "Draggable item with a plain text",
Subtitle = "A sample page demonstrating how to drag and drop data",
DataPackage = dataPackageWithText,
},
new ListItem(new NoOpCommand())
{
Title = "Draggable item with a lazily rendered plain text",
Subtitle = "A sample page demonstrating how to drag and drop data with delayed rendering",
DataPackage = dataPackageWithDelayedText,
},
new ListItem(new NoOpCommand())
{
Title = "Draggable item with an image",
Subtitle = "This item has an image - package contains both file and a bitmap",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
DataPackage = dataPackageWithImage,
},
new ListItem(new SampleDataTransferOnGridPage())
{
Title = "Drag & drop grid",
Subtitle = "A sample page demonstrating a grid list of items",
Icon = new IconInfo("\uF0E2"),
}
];
}
private static DataPackage CreateDataPackageWithText()
{
var dataPackageWithText = new DataPackage
{
Properties =
{
Title = "Item with data package with text",
Description = "This item has associated text with it",
},
RequestedOperation = DataPackageOperation.Copy,
};
dataPackageWithText.SetText("Text data in the Data Package");
return dataPackageWithText;
}
private static DataPackage CreateDataPackageWithDelayedText()
{
var dataPackageWithDelayedText = new DataPackage
{
Properties =
{
Title = "Item with delayed render data in the data package",
Description = "This items has an item associated with it that is evaluated when requested for the first time",
},
RequestedOperation = DataPackageOperation.Copy,
};
dataPackageWithDelayedText.SetDataProvider(StandardDataFormats.Text, request =>
{
var d = request.GetDeferral();
try
{
request.SetData(DateTime.Now.ToString("G", CultureInfo.CurrentCulture));
}
finally
{
d.Complete();
}
});
return dataPackageWithDelayedText;
}
private static DataPackage CreateDataPackageWithImage()
{
var dataPackageWithImage = new DataPackage
{
Properties =
{
Title = "Item with delayed render image in the data package",
Description = "This items has an image associated with it that is evaluated when requested for the first time",
},
RequestedOperation = DataPackageOperation.Copy,
};
dataPackageWithImage.SetDataProvider(StandardDataFormats.Bitmap, async void (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Images/Swirls.png"));
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
var streamRef = RandomAccessStreamReference.CreateFromStream(stream);
request.SetData(streamRef);
}
finally
{
deferral.Complete();
}
});
dataPackageWithImage.SetDataProvider(StandardDataFormats.StorageItems, async void (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Images/Swirls.png"));
var items = new[] { file };
request.SetData(items);
}
finally
{
deferral.Complete();
}
});
return dataPackageWithImage;
}
public override IListItem[] GetItems() => _items;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Samples")]
internal sealed partial class SampleDataTransferOnGridPage : ListPage
{
public SampleDataTransferOnGridPage()
{
GridProperties = new GalleryGridLayout
{
ShowTitle = true,
ShowSubtitle = true,
};
}
public override IListItem[] GetItems()
{
return [
new ListItem(new NoOpCommand())
{
Title = "Red Rectangle",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/RedRectangle.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Swirls",
Subtitle = "Drop me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Windows Digital",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Win-Digital.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Red Rectangle",
Subtitle = "Drop me",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/RedRectangle.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Space",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Space.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Swirls",
Subtitle = "Drop me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Windows Digital",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Win-Digital.png"),
},
];
}
private static DataPackage CreateDataPackageForImage(string relativePath)
{
var dataPackageWithImage = new DataPackage
{
Properties =
{
Title = "Image",
Description = "This item has an image associated with it.",
},
RequestedOperation = DataPackageOperation.Copy,
};
var imageUri = new Uri($"ms-appx:///{relativePath}");
dataPackageWithImage.SetDataProvider(StandardDataFormats.Bitmap, async (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(imageUri);
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
var streamRef = RandomAccessStreamReference.CreateFromStream(stream);
request.SetData(streamRef);
}
finally
{
deferral.Complete();
}
});
dataPackageWithImage.SetDataProvider(StandardDataFormats.StorageItems, async (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(imageUri);
var items = new[] { file };
request.SetData(items);
}
finally
{
deferral.Complete();
}
});
return dataPackageWithImage;
}
}

View File

@@ -24,6 +24,11 @@ public partial class SamplesListPage : ListPage
Title = "List Page With Details",
Subtitle = "A list of items, each with additional details to display",
},
new ListItem(new SectionsIndexPage())
{
Title = "List Pages With Sections",
Subtitle = "A list of items, with sections header",
},
new ListItem(new SampleUpdatingItemsPage())
{
Title = "List page with items that change",
@@ -101,6 +106,13 @@ public partial class SamplesListPage : ListPage
Subtitle = "A demo of the settings helpers",
},
// Data package samples
new ListItem(new SampleDataTransferPage())
{
Title = "Clipboard and Drag-and-Drop Demo",
Subtitle = "Demonstrates clipboard integration and drag-and-drop functionality",
},
// Evil edge cases
// Anything weird that might break the palette - put that in here.
new ListItem(new EvilSamplesPage())

View File

@@ -2,14 +2,23 @@
// 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.ApplicationModel.DataTransfer;
using Windows.Foundation.Collections;
using WinRT;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class CommandItem : BaseObservable, ICommandItem
public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttributesProvider
{
private readonly PropertySet _extendedAttributes = new();
private ICommand? _command;
private WeakEventListener<CommandItem, object, IPropChangedEventArgs>? _commandListener;
private string _title = string.Empty;
private DataPackage? _dataPackage;
private DataPackageView? _dataPackageView;
public virtual IIconInfo? Icon
{
get => field;
@@ -91,6 +100,32 @@ public partial class CommandItem : BaseObservable, ICommandItem
= [];
public DataPackage? DataPackage
{
get => _dataPackage;
set
{
_dataPackage = value;
_dataPackageView = null;
_extendedAttributes[WellKnownExtensionAttributes.DataPackage] = value?.AsAgile().Get()?.GetView()!;
OnPropertyChanged(nameof(DataPackage));
OnPropertyChanged(nameof(DataPackageView));
}
}
public DataPackageView? DataPackageView
{
get => _dataPackageView;
set
{
_dataPackage = null;
_dataPackageView = value;
_extendedAttributes[WellKnownExtensionAttributes.DataPackage] = value?.AsAgile().Get()!;
OnPropertyChanged(nameof(DataPackage));
OnPropertyChanged(nameof(DataPackageView));
}
}
public CommandItem()
: this(new NoOpCommand())
{
@@ -132,4 +167,9 @@ public partial class CommandItem : BaseObservable, ICommandItem
Title = title;
Subtitle = subtitle;
}
public IDictionary<string, object> GetProperties()
{
return _extendedAttributes;
}
}

View File

@@ -1,10 +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.
using Windows.Foundation.Collections;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class Details : BaseObservable, IDetails
public partial class Details : BaseObservable, IDetails, IExtendedAttributesProvider
{
public virtual IIconInfo HeroImage
{
@@ -53,4 +54,21 @@ public partial class Details : BaseObservable, IDetails
}
= [];
public virtual ContentSize Size
{
get;
set
{
field = value;
OnPropertyChanged(nameof(Size));
}
}
= ContentSize.Small;
public IDictionary<string, object>? GetProperties() => new ValueSet()
{
{ "Size", (int)Size },
};
}

View File

@@ -27,6 +27,6 @@ public partial class FontIconData : IconData, IExtendedAttributesProvider
public IDictionary<string, object>? GetProperties() => new ValueSet()
{
{ "FontFamily", FontFamily },
{ WellKnownExtensionAttributes.FontFamily, FontFamily },
};
}

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 System.Collections;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public sealed partial class Section : IEnumerable<IListItem>
{
public IListItem[] Items { get; set; } = [];
public string SectionTitle { get; set; } = string.Empty;
private Separator CreateSectionListItem()
{
return new Separator(SectionTitle);
}
public Section(string sectionName, IListItem[] items)
{
SectionTitle = sectionName;
var listItems = items.ToList();
if (listItems.Count > 0)
{
listItems.Insert(0, CreateSectionListItem());
Items = [.. listItems];
}
}
public Section()
{
}
public IEnumerator<IListItem> GetEnumerator() => Items.ToList().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -4,6 +4,40 @@
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
{
public Separator(string? title = "")
: base()
{
Section = title ?? string.Empty;
Command = null;
}
public IDetails? Details => null;
public string? Section { get; private set; }
public ITag[]? Tags => null;
public string? TextToSuggest => null;
public ICommand? Command { get; private set; }
public IIconInfo? Icon => null;
public IContextItem[]? MoreCommands => null;
public string? Subtitle => null;
public string? Title
{
get => Section;
set => Section = value;
}
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
}

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.CommandPalette.Extensions.Toolkit;
public static class WellKnownExtensionAttributes
{
public const string DataPackage = "Microsoft.CommandPalette.DataPackage";
public const string FontFamily = "FontFamily";
}

View File

@@ -160,6 +160,15 @@ namespace Microsoft.CommandPalette.Extensions
[uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")]
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IDetailsData {}
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
enum ContentSize
{
Small = 0,
Medium = 1,
Large = 2,
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IDetailsElement {
String Key { get; };

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using FancyZonesEditor.Models;
@@ -49,19 +50,16 @@ namespace UITests_FancyZones
[TestInitialize]
public void TestInitialize()
{
// ClearOpenWindows
Session.KillAllProcessesByName("PowerToys");
ClearOpenWindows();
// kill all processes related to FancyZones Editor to ensure a clean state
Session.KillAllProcessesByName("PowerToys.FancyZonesEditor");
AppZoneHistory.DeleteFile();
this.RestartScopeExe();
FancyZonesEditorHelper.Files.Restore();
// Set a custom layout with 1 subzones and clear app zone history
SetupCustomLayouts();
RestartScopeExe("Hosts");
Thread.Sleep(2000);
// Get the current mouse button setting
nonPrimaryMouseButton = SystemInformation.MouseButtonsSwapped ? "Left" : "Right";
@@ -72,99 +70,6 @@ namespace UITests_FancyZones
LaunchFancyZones();
}
/// <summary>
/// Test Use Shift key to activate zones while dragging a window in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that holding Shift while dragging shows all zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnShiftDuringDrag")]
[TestCategory("FancyZones_Dragging #1")]
public void TestShowZonesOnShiftDuringDrag()
{
string testCaseName = nameof(TestShowZonesOnShiftDuringDrag);
Pane dragElement = Find<Pane>(By.Name("Non Client Input Sink Window")); // element to drag
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
},
postAction: () =>
{
Session.PressKey(Key.Shift);
Task.Delay(500).Wait();
},
releaseAction: () =>
{
Session.ReleaseKey(Key.Shift);
Task.Delay(5000).Wait(); // Optional: Wait for a moment to ensure window switch
},
testCaseName: testCaseName);
string zoneColorWithoutShift = GetOutWindowPixelColor(30);
Assert.AreNotEqual(initialColor, withShiftColor, $"[{testCaseName}] Zone display failed.");
Assert.IsTrue(
withShiftColor == inactivateColor || withShiftColor == highlightColor,
$"[{testCaseName}] Zone display failed: withShiftColor was {withShiftColor}, expected {inactivateColor} or {highlightColor}.");
Assert.AreEqual(inactivateColor, withShiftColor, $"[{testCaseName}] Zone display failed.");
Assert.AreEqual(zoneColorWithoutShift, initialColor, $"[{testCaseName}] Zone deactivated failed.");
dragElement.ReleaseDrag();
Clean();
}
/// <summary>
/// Test dragging a window during Shift key press in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that dragging activates zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnDragDuringShift")]
[TestCategory("FancyZones_Dragging #2")]
public void TestShowZonesOnDragDuringShift()
{
string testCaseName = nameof(TestShowZonesOnDragDuringShift);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withDragColor) = RunDragInteractions(
preAction: () =>
{
dragElement.Drag(offSet.Dx, offSet.Dy);
Session.PressKey(Key.Shift);
},
postAction: () =>
{
dragElement.DragAndHold(0, 0);
Task.Delay(5000).Wait();
},
releaseAction: () =>
{
dragElement.ReleaseDrag();
Session.ReleaseKey(Key.Shift);
},
testCaseName: testCaseName);
Assert.AreNotEqual(initialColor, withDragColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withDragColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
// double check by app-zone-history.json
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneNumber = ZoneSwitchHelper.GetZoneIndexSetByAppName(powertoysWindowName, appZoneHistoryJson);
Assert.IsNull(zoneNumber, $"[{testCaseName}] AppZoneHistory layout was unexpectedly set.");
Clean();
}
/// <summary>
/// Test toggling zones using a non-primary mouse click during window dragging.
/// <list type="bullet">
@@ -178,14 +83,19 @@ namespace UITests_FancyZones
public void TestToggleZonesWithNonPrimaryMouseClick()
{
string testCaseName = nameof(TestToggleZonesWithNonPrimaryMouseClick);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withMouseColor) = RunDragInteractions(
preAction: () =>
{
// activate zone
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
@@ -195,7 +105,7 @@ namespace UITests_FancyZones
},
releaseAction: () =>
{
dragElement.ReleaseDrag();
Session.PerformMouseAction(MouseActionType.LeftUp);
},
testCaseName: testCaseName);
@@ -204,8 +114,6 @@ namespace UITests_FancyZones
// check the zone color is activated
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Clean();
}
/// <summary>
@@ -221,32 +129,35 @@ namespace UITests_FancyZones
public void TestShowZonesWhenShiftAndMouseOff()
{
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOff);
Pane dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
// activate zone
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
// press Shift Key to deactivate zones
Session.PressKey(Key.Shift);
Task.Delay(500).Wait();
Task.Delay(1000).Wait();
},
releaseAction: () =>
{
dragElement.ReleaseDrag();
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
},
testCaseName: testCaseName);
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Assert.AreNotEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone deactivation failed.");
Clean();
}
/// <summary>
@@ -263,12 +174,17 @@ namespace UITests_FancyZones
{
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOn);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
@@ -279,7 +195,7 @@ namespace UITests_FancyZones
},
testCaseName: testCaseName);
Assert.AreEqual(inactivateColor, withShiftColor, $"[{testCaseName}] show zone failed.");
Assert.AreEqual(highlightColor, withShiftColor, $"[{testCaseName}] show zone failed.");
Session.PerformMouseAction(
nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick);
@@ -288,9 +204,7 @@ namespace UITests_FancyZones
Assert.AreEqual(initialColor, zoneColorWithMouse, $"[{nameof(TestShowZonesWhenShiftAndMouseOff)}] Zone deactivate failed.");
Session.ReleaseKey(Key.Shift);
dragElement.ReleaseDrag();
Clean();
Session.PerformMouseAction(MouseActionType.LeftUp);
}
/// <summary>
@@ -307,8 +221,6 @@ namespace UITests_FancyZones
{
var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreNotEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOn)}] Window transparency failed.");
Clean();
}
/// <summary>
@@ -325,14 +237,103 @@ namespace UITests_FancyZones
{
var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOff)}] Window without transparency failed.");
Clean();
}
private void Clean()
/// <summary>
/// Test Use Shift key to activate zones while dragging a window in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that holding Shift while dragging shows all zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnShiftDuringDrag")]
[TestCategory("FancyZones_Dragging #1")]
public void TestShowZonesOnShiftDuringDrag()
{
// clean app zone history file
AppZoneHistory.DeleteFile();
string testCaseName = nameof(TestShowZonesOnShiftDuringDrag);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
Session.PressKey(Key.Shift);
Task.Delay(500).Wait();
},
releaseAction: () =>
{
Session.ReleaseKey(Key.Shift);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure window switch
},
testCaseName: testCaseName);
string zoneColorWithoutShift = GetOutWindowPixelColor(30);
Assert.AreNotEqual(initialColor, withShiftColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
Session.PerformMouseAction(MouseActionType.LeftUp);
}
/// <summary>
/// Test dragging a window during Shift key press in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that dragging activates zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnDragDuringShift")]
[TestCategory("FancyZones_Dragging #2")]
public void TestShowZonesOnDragDuringShift()
{
string testCaseName = nameof(TestShowZonesOnDragDuringShift);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withDragColor) = RunDragInteractions(
preAction: () =>
{
Session.PressKey(Key.Shift);
Task.Delay(100).Wait();
},
postAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
Task.Delay(1000).Wait();
},
releaseAction: () =>
{
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(100).Wait();
},
testCaseName: testCaseName);
Assert.AreNotEqual(initialColor, withDragColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withDragColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
// double check by app-zone-history.json
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneNumber = ZoneSwitchHelper.GetZoneIndexSetByAppName(powertoysWindowName, appZoneHistoryJson);
Assert.IsNull(zoneNumber, $"[{testCaseName}] AppZoneHistory layout was unexpectedly set.");
}
// Helper method to ensure the desktop has no open windows by clicking the "Show Desktop" button
@@ -352,7 +353,7 @@ namespace UITests_FancyZones
desktopButtonName = "Show Desktop";
}
this.Find<Microsoft.PowerToys.UITest.Button>(By.Name(desktopButtonName), 5000, true).Click(false, 500, 2000);
this.Find<Microsoft.PowerToys.UITest.Button>(By.Name(desktopButtonName), 5000, true).Click(false, 500, 1000);
}
// Setup custom layout with 1 subzones
@@ -382,6 +383,11 @@ namespace UITests_FancyZones
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
ZoneBehaviourSettings(TestContext.TestName);
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000);
this.Session.Attach(PowerToysModule.FancyZone);
@@ -435,22 +441,26 @@ namespace UITests_FancyZones
// Get the mouse color of the pixel when make dragged window
private (string PixelInWindow, string TransPixel) GetPixelWhenMakeDraggedWindow()
{
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 100;
int endY = startY + 100;
// maximize the window to make sure get pixel color more accurate
dragElement.DoubleClick();
Session.MoveMouseTo(startX, startY);
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
// Session.PerformMouseAction(MouseActionType.LeftDoubleClick);
Session.PressKey(Key.Shift);
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure the window is in position
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
Tuple<int, int> pos = GetMousePosition();
string pixelInWindow = this.GetPixelColorString(pos.Item1, pos.Item2);
Session.ReleaseKey(Key.Shift);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure the window is in position
Task.Delay(1000).Wait();
string transPixel = this.GetPixelColorString(pos.Item1, pos.Item2);
dragElement.ReleaseDrag();
Session.PerformMouseAction(MouseActionType.LeftUp);
return (pixelInWindow, transPixel);
}

View File

@@ -271,7 +271,7 @@ namespace UITests_FancyZones
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
RestartScopeExe("Hosts");
}
[TestMethod("FancyZones.Settings.TestApplyHotKey")]
@@ -598,10 +598,12 @@ namespace UITests_FancyZones
this.TryReaction();
int tries = 24;
Pull(tries, "down"); // Pull the setting page up to make sure the setting is visible
this.Find<ToggleSwitch>("Enable quick layout switch").Toggle(flag);
this.Find<ToggleSwitch>("FancyZonesQuickLayoutSwitch").Toggle(flag);
tries = 24;
Pull(tries, "up");
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
}
private void TryReaction()

View File

@@ -34,7 +34,7 @@ namespace UITests_FancyZones
Session.KillAllProcessesByName("PowerToys.FancyZonesEditor");
AppZoneHistory.DeleteFile();
this.RestartScopeExe();
RestartScopeExe("Hosts");
FancyZonesEditorHelper.Files.Restore();
// Set a custom layout with 1 subzones and clear app zone history
@@ -137,7 +137,7 @@ namespace UITests_FancyZones
Task.Delay(500).Wait(); // Optional: Wait for a moment to ensure window switch
activeWindowTitle = ZoneSwitchHelper.GetActiveWindowTitle();
Assert.AreNotEqual(preWindow, activeWindowTitle);
Assert.AreEqual(postWindow, activeWindowTitle);
Clean(); // close the windows
}
@@ -151,9 +151,23 @@ namespace UITests_FancyZones
var rect = Session.GetMainWindowRect();
var (targetX, targetY) = ZoneSwitchHelper.GetScreenMargins(rect, 4);
var offSet = ZoneSwitchHelper.GetOffset(hostsView, targetX, targetY);
DragWithShift(hostsView, offSet);
// Snap first window (Hosts) to left zone using shift+drag with direct mouse movement
var hostsRect = hostsView.Rect ?? throw new InvalidOperationException("Failed to get hosts window rect");
int hostsStartX = hostsRect.Left + 70;
int hostsStartY = hostsRect.Top + 25;
// For a 2-column layout, left zone is at approximately 1/4 of screen width
int hostsEndX = rect.Left + (3 * (rect.Right - rect.Left) / 4);
int hostsEndY = rect.Top + ((rect.Bottom - rect.Top) / 2);
Session.MoveMouseTo(hostsStartX, hostsStartY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.PressKey(Key.Shift);
Session.MoveMouseTo(hostsEndX, hostsEndY);
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(500).Wait(); // Wait for snap to complete
string preWindow = ZoneSwitchHelper.GetActiveWindowTitle();
@@ -163,11 +177,26 @@ namespace UITests_FancyZones
Pane settingsView = Find<Pane>(By.Name("Non Client Input Sink Window"));
settingsView.DoubleClick(); // maximize the window
DragWithShift(settingsView, offSet);
var windowRect = Session.GetMainWindowRect();
var settingsRect = settingsView.Rect ?? throw new InvalidOperationException("Failed to get settings window rect");
int settingsStartX = settingsRect.Left + 70;
int settingsStartY = settingsRect.Top + 25;
// For a 2-column layout, right zone is at approximately 3/4 of screen width
int settingsEndX = windowRect.Left + (3 * (windowRect.Right - windowRect.Left) / 4);
int settingsEndY = windowRect.Top + ((windowRect.Bottom - windowRect.Top) / 2);
Session.MoveMouseTo(settingsStartX, settingsStartY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.PressKey(Key.Shift);
Session.MoveMouseTo(settingsEndX, settingsEndY);
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(500).Wait(); // Wait for snap to complete
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneIndexOfFileWindow = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Hosts.exe", appZoneHistoryJson); // explorer.exe
string? zoneIndexOfFileWindow = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Hosts.exe", appZoneHistoryJson);
string? zoneIndexOfPowertoys = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Settings.exe", appZoneHistoryJson);
// check the AppZoneHistory layout is set and in the same zone
@@ -176,16 +205,6 @@ namespace UITests_FancyZones
return (preWindow, powertoysWindowName);
}
private void DragWithShift(Pane settingsView, (int Dx, int Dy) offSet)
{
Session.PressKey(Key.Shift);
settingsView.DragAndHold(offSet.Dx, offSet.Dy);
Task.Delay(1000).Wait(); // Wait for drag to start (optional)
settingsView.ReleaseDrag();
Task.Delay(1000).Wait(); // Wait after drag (optional)
Session.ReleaseKey(Key.Shift);
}
private static readonly CustomLayouts.CustomLayoutListWrapper CustomLayoutsList = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
@@ -253,11 +272,14 @@ namespace UITests_FancyZones
this.Scroll(9, "Down"); // Pull the setting page up to make sure the setting is visible
bool switchWindowEnable = TestContext.TestName == "TestSwitchShortCutDisable" ? false : true;
this.Find<ToggleSwitch>("Switch between windows in the current zone").Toggle(switchWindowEnable);
this.Find<ToggleSwitch>("FancyZonesWindowSwitchingToggle").Toggle(switchWindowEnable);
Task.Delay(500).Wait(); // Wait for the setting to be applied
this.Scroll(9, "Up"); // Pull the setting page down to make sure the setting is visible
this.Find<Button>("Launch layout editor").Click(false, 500, 5000);
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<Button>("Open layout editor").Click(false, 500, 5000);
this.Session.Attach(PowerToysModule.FancyZone);
// pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required.
@@ -273,7 +295,7 @@ namespace UITests_FancyZones
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
this.Session.Attach(PowerToysModule.PowerToysSettings);
SetupCustomLayouts();
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 5000, 5000);
this.Find<Microsoft.PowerToys.UITest.Button>("Open layout editor").Click(false, 5000, 5000);
this.Session.Attach(PowerToysModule.FancyZone);
// customLayoutData = FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.GetData();
@@ -301,11 +323,11 @@ namespace UITests_FancyZones
Task.Delay(1000).Wait();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Open as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
// launch Hosts File Editor
this.Find<Button>("Launch Hosts File Editor").Click();
this.Find<Button>("Open Hosts File Editor").Click();
Task.Delay(5000).Wait();
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.ImageResizerCLI</AssemblyTitle>
<AssemblyDescription>PowerToys Image Resizer Command Line Interface</AssemblyDescription>
<Description>PowerToys Image Resizer CLI</Description>
<OutputType>Exe</OutputType>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
<AssemblyName>PowerToys.ImageResizerCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Text;
using ImageResizer.Cli;
using ManagedCommon;
namespace ImageResizerCLI;
internal static class Program
{
private static int Main(string[] args)
{
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException)
{
// Ignore invalid culture and fall back to default.
}
Console.InputEncoding = Encoding.Unicode;
// Initialize logger to file (same as other modules)
CliLogger.Initialize("\\ImageResizer\\Logs");
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
try
{
var executor = new ImageResizerCliExecutor();
return executor.Run(args);
}
catch (Exception ex)
{
CliLogger.Error($"Unhandled exception: {ex.Message}");
CliLogger.Error($"Stack trace: {ex.StackTrace}");
Console.Error.WriteLine($"Fatal error: {ex.Message}");
return 1;
}
}
}

View File

@@ -0,0 +1,320 @@
// 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 ImageResizer.Cli;
using ImageResizer.Models;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Cli
{
[TestClass]
public class CliSettingsApplierTests
{
private Settings CreateDefaultSettings()
{
var settings = new Settings();
settings.Sizes.Add(new ResizeSize(0, "Small", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(1, "Medium", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(2, "Large", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel));
return settings;
}
[TestMethod]
public void Apply_WithCustomWidth_SetsCustomSizeWidth()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithCustomHeight_SetsCustomSizeHeight()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithCustomSize_SelectsCustomSizeIndex()
{
var options = new CliOptions { Width = 800, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size index should be settings.Sizes.Count
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithZeroWidth_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 0, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(0.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithZeroHeight_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 800, Height = 0 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(0.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithNullWidthAndHeight_DoesNotModifyCustomSize()
{
var options = new CliOptions { Width = null, Height = null };
var settings = CreateDefaultSettings();
var originalWidth = settings.CustomSize.Width;
var originalHeight = settings.CustomSize.Height;
CliSettingsApplier.Apply(options, settings);
// When both null, should not modify CustomSize (keeps default 1024x640)
Assert.AreEqual(originalWidth, settings.CustomSize.Width);
Assert.AreEqual(originalHeight, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithUnit_SetsCustomSizeUnit()
{
var options = new CliOptions { Width = 100, Unit = ResizeUnit.Percent };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
}
[TestMethod]
public void Apply_WithFit_SetsCustomSizeFit()
{
var options = new CliOptions { Width = 800, Fit = ResizeFit.Fill };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
}
[TestMethod]
public void Apply_WithValidSizeIndex_SetsSelectedSizeIndex()
{
var options = new CliOptions { SizeIndex = 1 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(1, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithInvalidSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = 99 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
// Should remain unchanged when invalid
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithNegativeSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = -1 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithShrinkOnly_SetsShrinkOnly()
{
var options = new CliOptions { ShrinkOnly = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.ShrinkOnly);
}
[TestMethod]
public void Apply_WithReplace_SetsReplace()
{
var options = new CliOptions { Replace = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.Replace);
}
[TestMethod]
public void Apply_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var options = new CliOptions { IgnoreOrientation = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.IgnoreOrientation);
}
[TestMethod]
public void Apply_WithRemoveMetadata_SetsRemoveMetadata()
{
var options = new CliOptions { RemoveMetadata = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.RemoveMetadata);
}
[TestMethod]
public void Apply_WithJpegQualityLevel_SetsJpegQualityLevel()
{
var options = new CliOptions { JpegQualityLevel = 85 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(85, settings.JpegQualityLevel);
}
[TestMethod]
public void Apply_WithKeepDateModified_SetsKeepDateModified()
{
var options = new CliOptions { KeepDateModified = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.KeepDateModified);
}
[TestMethod]
public void Apply_WithFileName_SetsFileName()
{
var options = new CliOptions { FileName = "%1 (%2)" };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual("%1 (%2)", settings.FileName);
}
[TestMethod]
public void Apply_WithEmptyFileName_DoesNotChangeFileName()
{
var options = new CliOptions { FileName = string.Empty };
var settings = CreateDefaultSettings();
var originalFileName = settings.FileName;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalFileName, settings.FileName);
}
[TestMethod]
public void Apply_WithMultipleOptions_AppliesAllOptions()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
Unit = ResizeUnit.Percent,
Fit = ResizeFit.Fill,
ShrinkOnly = true,
Replace = true,
IgnoreOrientation = true,
RemoveMetadata = true,
JpegQualityLevel = 90,
KeepDateModified = true,
FileName = "test_%2",
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
Assert.IsTrue(settings.ShrinkOnly);
Assert.IsTrue(settings.Replace);
Assert.IsTrue(settings.IgnoreOrientation);
Assert.IsTrue(settings.RemoveMetadata);
Assert.AreEqual(90, settings.JpegQualityLevel);
Assert.IsTrue(settings.KeepDateModified);
Assert.AreEqual("test_%2", settings.FileName);
}
[TestMethod]
public void Apply_CustomSizeTakesPrecedence_OverSizeIndex()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
SizeIndex = 1, // Should be ignored when Width/Height specified
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size should be selected, not preset
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyWidth_StillSelectsCustomSize()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyHeight_StillSelectsCustomSize()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
}
}

View File

@@ -0,0 +1,268 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using ImageResizer.Cli.Commands;
using ImageResizer.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Models
{
[TestClass]
public class CliOptionsTests
{
private static readonly string[] _multiFileArgs = new[] { "test1.jpg", "test2.jpg", "test3.jpg" };
private static readonly string[] _mixedOptionsArgs = new[] { "--width", "800", "test1.jpg", "--height", "600", "test2.jpg" };
[TestMethod]
public void Parse_WithValidWidth_SetsWidth()
{
var args = new[] { "--width", "800", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
}
[TestMethod]
public void Parse_WithValidHeight_SetsHeight()
{
var args = new[] { "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithShortWidthAlias_WorksIdentically()
{
var longFormArgs = new[] { "--width", "800", "test.jpg" };
var shortFormArgs = new[] { "-w", "800", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Width, shortForm.Width);
}
[TestMethod]
public void Parse_WithShortHeightAlias_WorksIdentically()
{
var longFormArgs = new[] { "--height", "600", "test.jpg" };
var shortFormArgs = new[] { "-h", "600", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Height, shortForm.Height);
}
[TestMethod]
public void Parse_WithValidUnit_SetsUnit()
{
var args = new[] { "--unit", "Percent", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Percent, options.Unit);
}
[TestMethod]
public void Parse_WithValidFit_SetsFit()
{
var args = new[] { "--fit", "Fill", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeFit.Fill, options.Fit);
}
[TestMethod]
public void Parse_WithSizeIndex_SetsSizeIndex()
{
var args = new[] { "--size", "2", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(2, options.SizeIndex);
}
[TestMethod]
public void Parse_WithShrinkOnly_SetsShrinkOnly()
{
var args = new[] { "--shrink-only", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ShrinkOnly);
}
[TestMethod]
public void Parse_WithReplace_SetsReplace()
{
var args = new[] { "--replace", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.Replace);
}
[TestMethod]
public void Parse_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var args = new[] { "--ignore-orientation", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.IgnoreOrientation);
}
[TestMethod]
public void Parse_WithRemoveMetadata_SetsRemoveMetadata()
{
var args = new[] { "--remove-metadata", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.RemoveMetadata);
}
[TestMethod]
public void Parse_WithValidQuality_SetsQuality()
{
var args = new[] { "--quality", "85", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(85, options.JpegQualityLevel);
}
[TestMethod]
public void Parse_WithKeepDateModified_SetsKeepDateModified()
{
var args = new[] { "--keep-date-modified", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.KeepDateModified);
}
[TestMethod]
public void Parse_WithFileName_SetsFileName()
{
var args = new[] { "--filename", "%1 (%2)", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("%1 (%2)", options.FileName);
}
[TestMethod]
public void Parse_WithDestination_SetsDestinationDirectory()
{
var args = new[] { "--destination", "C:\\Output", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("C:\\Output", options.DestinationDirectory);
}
[TestMethod]
public void Parse_WithShortDestinationAlias_WorksIdentically()
{
var longFormArgs = new[] { "--destination", "C:\\Output", "test.jpg" };
var shortFormArgs = new[] { "-d", "C:\\Output", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.DestinationDirectory, shortForm.DestinationDirectory);
}
[TestMethod]
public void Parse_WithProgressLines_SetsProgressLines()
{
var args = new[] { "--progress-lines", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithAccessibleAlias_SetsProgressLines()
{
var args = new[] { "--accessible", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithMultipleFiles_AddsAllFiles()
{
var args = _multiFileArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(3, options.Files.Count);
CollectionAssert.Contains(options.Files.ToList(), "test1.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test2.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test3.jpg");
}
[TestMethod]
public void Parse_WithMixedOptionsAndFiles_ParsesCorrectly()
{
var args = _mixedOptionsArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(600.0, options.Height);
Assert.AreEqual(2, options.Files.Count);
}
[TestMethod]
public void Parse_WithHelp_SetsShowHelp()
{
var args = new[] { "--help" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowHelp);
}
[TestMethod]
public void Parse_WithShowConfig_SetsShowConfig()
{
var args = new[] { "--show-config" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowConfig);
}
[TestMethod]
public void Parse_WithNoArguments_ReturnsEmptyOptions()
{
var args = Array.Empty<string>();
var options = CliOptions.Parse(args);
Assert.IsNotNull(options);
Assert.AreEqual(0, options.Files.Count);
}
[TestMethod]
public void Parse_WithZeroWidth_AllowsZeroValue()
{
var args = new[] { "--width", "0", "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(0.0, options.Width);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithZeroHeight_AllowsZeroValue()
{
var args = new[] { "--width", "800", "--height", "0", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(0.0, options.Height);
}
[TestMethod]
public void Parse_CaseInsensitiveEnums_ParsesCorrectly()
{
var args = new[] { "--unit", "pixel", "--fit", "fit", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Pixel, options.Unit);
Assert.AreEqual(ResizeFit.Fit, options.Fit);
}
}
}

View File

@@ -25,20 +25,27 @@ namespace ImageResizer.Models
[TestMethod]
public void FromCommandLineWorks()
{
// Use actual test files that exist in the test directory
var testDir = Path.GetDirectoryName(typeof(ResizeBatchTests).Assembly.Location);
var file1 = Path.Combine(testDir, "Test.jpg");
var file2 = Path.Combine(testDir, "Test.png");
var file3 = Path.Combine(testDir, "Test.gif");
var standardInput =
"Image1.jpg" + EOL +
"Image2.jpg";
file1 + EOL +
file2;
var args = new[]
{
"/d", "OutputDir",
"Image3.jpg",
file3,
};
var result = ResizeBatch.FromCommandLine(
new StringReader(standardInput),
args);
CollectionAssert.AreEquivalent(new List<string> { "Image1.jpg", "Image2.jpg", "Image3.jpg" }, result.Files.ToArray());
var files = result.Files.Select(Path.GetFileName).ToArray();
CollectionAssert.AreEquivalent(new List<string> { "Test.jpg", "Test.png", "Test.gif" }, files);
Assert.AreEqual("OutputDir", result.DestinationDirectory);
}

View File

@@ -6,9 +6,9 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using ImageResizer.Models;
using ImageResizer.Properties;
using ImageResizer.Utilities;
@@ -20,8 +20,32 @@ namespace ImageResizer
{
public partial class App : Application, IDisposable
{
private const string LogSubFolder = "\\ImageResizer\\Logs";
/// <summary>
/// Gets cached AI availability state, checked at app startup.
/// Can be updated after model download completes or background initialization.
/// </summary>
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
/// <summary>
/// Event fired when AI initialization completes in background.
/// Allows UI to refresh state when initialization finishes.
/// </summary>
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
static App()
{
try
{
// Initialize logger early (mirroring PowerOCR pattern)
Logger.InitializeLogger(LogSubFolder);
}
catch
{
/* swallow logger init issues silently */
}
try
{
string appLanguage = LanguageHelper.LoadLanguage();
@@ -30,9 +54,9 @@ namespace ImageResizer
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException)
catch (CultureNotFoundException ex)
{
// error
Logger.LogError("CultureNotFoundException: " + ex.Message);
}
Console.InputEncoding = Encoding.Unicode;
@@ -43,15 +67,59 @@ namespace ImageResizer
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
NativeMethods.SetProcessDPIAware();
// Check for AI detection mode (called by Runner in background)
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
{
RunAiDetectionMode();
return;
}
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
{
/* TODO: Add logs to ImageResizer.
* Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
*/
Logger.LogWarning("GPO policy disables ImageResizer. Exiting.");
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
return;
}
// AI Super Resolution is not supported on Windows 10 - skip cache check entirely
if (OSVersionHelper.IsWindows10())
{
AiAvailabilityState = AiAvailabilityState.NotSupported;
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
}
else
{
// Load AI availability from cache (written by Runner's background detection)
var cachedState = Services.AiAvailabilityCacheService.LoadCache();
if (cachedState.HasValue)
{
AiAvailabilityState = cachedState.Value;
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
}
else
{
// No valid cache - default to NotSupported (Runner will detect and cache for next startup)
AiAvailabilityState = AiAvailabilityState.NotSupported;
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
}
// If AI is potentially available, start background initialization (non-blocking)
if (AiAvailabilityState == AiAvailabilityState.Ready)
{
_ = InitializeAiServiceAsync(); // Fire and forget - don't block UI
}
else
{
// AI not available - set NoOp service immediately
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
}
}
var batch = ResizeBatch.FromCommandLine(Console.In, e?.Args);
// TODO: Add command-line parameters that can be used in lieu of the input page (issue #14)
@@ -62,9 +130,121 @@ namespace ImageResizer
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
}
/// <summary>
/// AI detection mode: perform detection, write to cache, and exit.
/// Called by Runner in background to avoid blocking ImageResizer UI startup.
/// </summary>
private void RunAiDetectionMode()
{
try
{
Logger.LogInfo("Running AI detection mode...");
// AI Super Resolution is not supported on Windows 10
if (OSVersionHelper.IsWindows10())
{
Logger.LogInfo("AI detection skipped: Windows 10 does not support AI Super Resolution");
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
Environment.Exit(0);
return;
}
// Perform detection (reuse existing logic)
var state = CheckAiAvailability();
// Write result to cache file
Services.AiAvailabilityCacheService.SaveCache(state);
Logger.LogInfo($"AI detection complete: {state}");
}
catch (Exception ex)
{
Logger.LogError($"AI detection failed: {ex.Message}");
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
}
// Exit silently without showing UI
Environment.Exit(0);
}
/// <summary>
/// Check AI Super Resolution availability on this system.
/// Performs architecture check and model availability check.
/// </summary>
private static AiAvailabilityState CheckAiAvailability()
{
try
{
// Check Windows AI service model ready state
// it's so slow, why?
var readyState = Services.WinAiSuperResolutionService.GetModelReadyState();
// Map AI service state to our availability state
switch (readyState)
{
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
return AiAvailabilityState.Ready;
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
return AiAvailabilityState.ModelNotReady;
case Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser:
case Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem:
default:
return AiAvailabilityState.NotSupported;
}
}
catch (Exception)
{
return AiAvailabilityState.NotSupported;
}
}
/// <summary>
/// Initialize AI Super Resolution service asynchronously in background.
/// Runs without blocking UI startup - state change event notifies completion.
/// </summary>
private static async System.Threading.Tasks.Task InitializeAiServiceAsync()
{
AiAvailabilityState finalState;
try
{
// Create and initialize AI service using async factory
var aiService = await Services.WinAiSuperResolutionService.CreateAsync();
if (aiService != null)
{
ResizeBatch.SetAiSuperResolutionService(aiService);
Logger.LogInfo("AI Super Resolution service initialized successfully.");
finalState = AiAvailabilityState.Ready;
}
else
{
// Initialization failed - use default NoOp service
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
finalState = AiAvailabilityState.NotSupported;
}
}
catch (Exception ex)
{
// Log error and use default NoOp service
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
finalState = AiAvailabilityState.NotSupported;
}
// Update cached state and notify listeners
AiAvailabilityState = finalState;
AiInitializationCompleted?.Invoke(null, finalState);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose AI Super Resolution service
ResizeBatch.DisposeAiSuperResolutionService();
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
namespace ImageResizer.Cli
{
public static class CliLogger
{
private static bool _initialized;
public static void Initialize(string logSubFolder)
{
if (!_initialized)
{
Logger.InitializeLogger(logSubFolder);
_initialized = true;
}
}
public static void Info(string message) => Logger.LogInfo(message);
public static void Warn(string message) => Logger.LogWarning(message);
public static void Error(string message) => Logger.LogError(message);
}
}

View File

@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Applies CLI options to Settings object.
/// Separated from executor logic for Single Responsibility Principle.
/// </summary>
public static class CliSettingsApplier
{
/// <summary>
/// Applies CLI options to the settings, overriding default values.
/// </summary>
/// <param name="cliOptions">The CLI options to apply.</param>
/// <param name="settings">The settings to modify.</param>
public static void Apply(CliOptions cliOptions, Settings settings)
{
// Handle complex size options first
ApplySizeOptions(cliOptions, settings);
// Apply simple property mappings
ApplySimpleOptions(cliOptions, settings);
}
private static void ApplySizeOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.Width.HasValue || cliOptions.Height.HasValue)
{
ApplyCustomSizeOptions(cliOptions, settings);
}
else if (cliOptions.SizeIndex.HasValue)
{
ApplyPresetSizeOption(cliOptions, settings);
}
}
private static void ApplyCustomSizeOptions(CliOptions cliOptions, Settings settings)
{
// Set dimensions (0 = auto-calculate for aspect ratio preservation)
// Implementation: ResizeSize.ConvertToPixels() returns double.PositiveInfinity for 0 in Fit mode,
// causing Math.Min(scaleX, scaleY) to preserve aspect ratio by selecting the non-zero scale.
// For Fill/Stretch modes, 0 uses the original dimension instead.
settings.CustomSize.Width = cliOptions.Width ?? 0;
settings.CustomSize.Height = cliOptions.Height ?? 0;
// Apply optional properties
if (cliOptions.Unit.HasValue)
{
settings.CustomSize.Unit = cliOptions.Unit.Value;
}
if (cliOptions.Fit.HasValue)
{
settings.CustomSize.Fit = cliOptions.Fit.Value;
}
// Select custom size (index = Sizes.Count)
settings.SelectedSizeIndex = settings.Sizes.Count;
}
private static void ApplyPresetSizeOption(CliOptions cliOptions, Settings settings)
{
var index = cliOptions.SizeIndex.Value;
if (index >= 0 && index < settings.Sizes.Count)
{
settings.SelectedSizeIndex = index;
}
else
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_WarningInvalidSizeIndex, index));
CliLogger.Warn($"Invalid size index: {index}");
}
}
private static void ApplySimpleOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.ShrinkOnly.HasValue)
{
settings.ShrinkOnly = cliOptions.ShrinkOnly.Value;
}
if (cliOptions.Replace.HasValue)
{
settings.Replace = cliOptions.Replace.Value;
}
if (cliOptions.IgnoreOrientation.HasValue)
{
settings.IgnoreOrientation = cliOptions.IgnoreOrientation.Value;
}
if (cliOptions.RemoveMetadata.HasValue)
{
settings.RemoveMetadata = cliOptions.RemoveMetadata.Value;
}
if (cliOptions.JpegQualityLevel.HasValue)
{
settings.JpegQualityLevel = cliOptions.JpegQualityLevel.Value;
}
if (cliOptions.KeepDateModified.HasValue)
{
settings.KeepDateModified = cliOptions.KeepDateModified.Value;
}
if (!string.IsNullOrEmpty(cliOptions.FileName))
{
settings.FileName = cliOptions.FileName;
}
}
}
}

View File

@@ -0,0 +1,90 @@
// 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.CommandLine;
using ImageResizer.Cli.Options;
namespace ImageResizer.Cli.Commands
{
/// <summary>
/// Root command for the ImageResizer CLI.
/// </summary>
public sealed class ImageResizerRootCommand : RootCommand
{
public ImageResizerRootCommand()
: base("PowerToys Image Resizer - Resize images from command line")
{
HelpOption = new HelpOption();
ShowConfigOption = new ShowConfigOption();
DestinationOption = new DestinationOption();
WidthOption = new WidthOption();
HeightOption = new HeightOption();
UnitOption = new UnitOption();
FitOption = new FitOption();
SizeOption = new SizeOption();
ShrinkOnlyOption = new ShrinkOnlyOption();
ReplaceOption = new ReplaceOption();
IgnoreOrientationOption = new IgnoreOrientationOption();
RemoveMetadataOption = new RemoveMetadataOption();
QualityOption = new QualityOption();
KeepDateModifiedOption = new KeepDateModifiedOption();
FileNameOption = new FileNameOption();
ProgressLinesOption = new ProgressLinesOption();
FilesArgument = new FilesArgument();
AddOption(HelpOption);
AddOption(ShowConfigOption);
AddOption(DestinationOption);
AddOption(WidthOption);
AddOption(HeightOption);
AddOption(UnitOption);
AddOption(FitOption);
AddOption(SizeOption);
AddOption(ShrinkOnlyOption);
AddOption(ReplaceOption);
AddOption(IgnoreOrientationOption);
AddOption(RemoveMetadataOption);
AddOption(QualityOption);
AddOption(KeepDateModifiedOption);
AddOption(FileNameOption);
AddOption(ProgressLinesOption);
AddArgument(FilesArgument);
}
public HelpOption HelpOption { get; }
public ShowConfigOption ShowConfigOption { get; }
public DestinationOption DestinationOption { get; }
public WidthOption WidthOption { get; }
public HeightOption HeightOption { get; }
public UnitOption UnitOption { get; }
public FitOption FitOption { get; }
public SizeOption SizeOption { get; }
public ShrinkOnlyOption ShrinkOnlyOption { get; }
public ReplaceOption ReplaceOption { get; }
public IgnoreOrientationOption IgnoreOrientationOption { get; }
public RemoveMetadataOption RemoveMetadataOption { get; }
public QualityOption QualityOption { get; }
public KeepDateModifiedOption KeepDateModifiedOption { get; }
public FileNameOption FileNameOption { get; }
public ProgressLinesOption ProgressLinesOption { get; }
public FilesArgument FilesArgument { get; }
}
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Executes Image Resizer CLI operations.
/// Instance-based design for better testability and Single Responsibility Principle.
/// </summary>
public class ImageResizerCliExecutor
{
/// <summary>
/// Runs the CLI executor with the provided command-line arguments.
/// </summary>
/// <param name="args">Command-line arguments.</param>
/// <returns>Exit code.</returns>
public int Run(string[] args)
{
var cliOptions = CliOptions.Parse(args);
if (cliOptions.ParseErrors.Count > 0)
{
foreach (var error in cliOptions.ParseErrors)
{
Console.Error.WriteLine(error);
CliLogger.Error($"Parse error: {error}");
}
CliOptions.PrintUsage();
return 1;
}
if (cliOptions.ShowHelp)
{
CliOptions.PrintUsage();
return 0;
}
if (cliOptions.ShowConfig)
{
CliOptions.PrintConfig(Settings.Default);
return 0;
}
if (cliOptions.Files.Count == 0 && string.IsNullOrEmpty(cliOptions.PipeName))
{
Console.WriteLine(Resources.CLI_NoInputFiles);
CliOptions.PrintUsage();
return 1;
}
return RunSilentMode(cliOptions);
}
private int RunSilentMode(CliOptions cliOptions)
{
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
var settings = Settings.Default;
CliSettingsApplier.Apply(cliOptions, settings);
CliLogger.Info($"CLI mode: processing {batch.Files.Count} files");
// Use accessible line-based progress if requested or detected
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
int lastReportedMilestone = -1;
var errors = batch.Process(
(completed, total) =>
{
var progress = (int)((completed / total) * 100);
if (useLineBasedProgress)
{
// Milestone-based progress (0%, 25%, 50%, 75%, 100%)
int milestone = (progress / 25) * 25;
if (milestone > lastReportedMilestone || completed == (int)total)
{
lastReportedMilestone = milestone;
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total));
}
}
else
{
// Traditional carriage return mode
Console.Write(string.Format(CultureInfo.InvariantCulture, "\r{0}", string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total)));
}
},
settings,
CancellationToken.None);
if (!useLineBasedProgress)
{
Console.WriteLine();
}
var errorList = errors.ToList();
if (errorList.Count > 0)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_CompletedWithErrors, errorList.Count));
CliLogger.Error($"Processing completed with {errorList.Count} error(s)");
foreach (var error in errorList)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, " {0}: {1}", error.File, error.Error));
CliLogger.Error($" {error.File}: {error.Error}");
}
return 1;
}
CliLogger.Info("CLI batch completed successfully");
Console.WriteLine(Resources.CLI_AllFilesProcessed);
return 0;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class DestinationOption : Option<string>
{
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FileNameOption : Option<string>
{
private static readonly string[] _aliases = ["--filename", "-n"];
public FileNameOption()
: base(_aliases, Properties.Resources.CLI_Option_FileName)
{
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FilesArgument : Argument<string[]>
{
public FilesArgument()
: base("files", Properties.Resources.CLI_Option_Files)
{
Arity = ArgumentArity.ZeroOrMore;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FitOption : Option<ImageResizer.Models.ResizeFit?>
{
private static readonly string[] _aliases = ["--fit", "-f"];
public FitOption()
: base(_aliases, Properties.Resources.CLI_Option_Fit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HeightOption : Option<double?>
{
private static readonly string[] _aliases = ["--height", "-h"];
public HeightOption()
: base(_aliases, Properties.Resources.CLI_Option_Height)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HelpOption : Option<bool>
{
private static readonly string[] _aliases = ["--help", "-?", "/?"];
public HelpOption()
: base(_aliases, Properties.Resources.CLI_Option_Help)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class IgnoreOrientationOption : Option<bool>
{
private static readonly string[] _aliases = ["--ignore-orientation"];
public IgnoreOrientationOption()
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class KeepDateModifiedOption : Option<bool>
{
private static readonly string[] _aliases = ["--keep-date-modified"];
public KeepDateModifiedOption()
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ProgressLinesOption : Option<bool>
{
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
public ProgressLinesOption()
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
{
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class QualityOption : Option<int?>
{
private static readonly string[] _aliases = ["--quality", "-q"];
public QualityOption()
: base(_aliases, Properties.Resources.CLI_Option_Quality)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && (value.Value < 1 || value.Value > 100))
{
result.ErrorMessage = "JPEG quality must be between 1 and 100.";
}
});
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class RemoveMetadataOption : Option<bool>
{
private static readonly string[] _aliases = ["--remove-metadata"];
public RemoveMetadataOption()
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ReplaceOption : Option<bool>
{
private static readonly string[] _aliases = ["--replace", "-r"];
public ReplaceOption()
: base(_aliases, Properties.Resources.CLI_Option_Replace)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShowConfigOption : Option<bool>
{
private static readonly string[] _aliases = ["--show-config", "--config"];
public ShowConfigOption()
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShrinkOnlyOption : Option<bool>
{
private static readonly string[] _aliases = ["--shrink-only"];
public ShrinkOnlyOption()
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
{
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class SizeOption : Option<int?>
{
private static readonly string[] _aliases = ["--size"];
public SizeOption()
: base(_aliases, Properties.Resources.CLI_Option_Size)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && value.Value < 0)
{
result.ErrorMessage = "Size index must be a non-negative integer.";
}
});
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class UnitOption : Option<ImageResizer.Models.ResizeUnit?>
{
private static readonly string[] _aliases = ["--unit", "-u"];
public UnitOption()
: base(_aliases, Properties.Resources.CLI_Option_Unit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class WidthOption : Option<double?>
{
private static readonly string[] _aliases = ["--width", "-w"];
public WidthOption()
: base(_aliases, Properties.Resources.CLI_Option_Width)
{
}
}
}

View File

@@ -10,6 +10,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<UseWPF>true</UseWPF>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
</PropertyGroup>
<PropertyGroup>
@@ -18,19 +19,21 @@
<RootNamespace>ImageResizer</RootNamespace>
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>CA1863</NoWarn>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
</PropertyGroup>
<!-- <PropertyGroup>
<PropertyGroup>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
</PropertyGroup> -->
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
@@ -46,7 +49,10 @@
<Resource Include="Resources\ImageResizer.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WPF-UI" />
</ItemGroup>

View File

@@ -0,0 +1,41 @@
// 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.Globalization;
using System.Text;
using System.Text.Json.Serialization;
using ImageResizer.Properties;
namespace ImageResizer.Models
{
public class AiSize : ResizeSize
{
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
private int _scale = 2;
/// <summary>
/// Gets the formatted scale display string (e.g., "2×").
/// </summary>
[JsonIgnore]
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
[JsonPropertyName("scale")]
public int Scale
{
get => _scale;
set => Set(ref _scale, value);
}
[JsonConstructor]
public AiSize(int scale)
{
Scale = scale;
}
public AiSize()
{
}
}
}

View File

@@ -0,0 +1,261 @@
// 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.Collections.ObjectModel;
using System.CommandLine.Parsing;
using System.Globalization;
using ImageResizer.Cli.Commands;
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type
namespace ImageResizer.Models
{
/// <summary>
/// Represents the command-line options for ImageResizer CLI mode.
/// </summary>
public class CliOptions
{
/// <summary>
/// Gets or sets a value indicating whether to show help information.
/// </summary>
public bool ShowHelp { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show current configuration.
/// </summary>
public bool ShowConfig { get; set; }
/// <summary>
/// Gets or sets the destination directory for resized images.
/// </summary>
public string DestinationDirectory { get; set; }
/// <summary>
/// Gets or sets the width of the resized image.
/// </summary>
public double? Width { get; set; }
/// <summary>
/// Gets or sets the height of the resized image.
/// </summary>
public double? Height { get; set; }
/// <summary>
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
/// </summary>
public ResizeUnit? Unit { get; set; }
/// <summary>
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
/// </summary>
public ResizeFit? Fit { get; set; }
/// <summary>
/// Gets or sets the index of the preset size to use.
/// </summary>
public int? SizeIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to only shrink images (not enlarge).
/// </summary>
public bool? ShrinkOnly { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to replace the original file.
/// </summary>
public bool? Replace { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore orientation when resizing.
/// </summary>
public bool? IgnoreOrientation { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to remove metadata from the resized image.
/// </summary>
public bool? RemoveMetadata { get; set; }
/// <summary>
/// Gets or sets the JPEG quality level (1-100).
/// </summary>
public int? JpegQualityLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to keep the date modified.
/// </summary>
public bool? KeepDateModified { get; set; }
/// <summary>
/// Gets or sets the output filename format.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use line-based progress output for screen reader accessibility.
/// </summary>
public bool? ProgressLines { get; set; }
/// <summary>
/// Gets the list of files to process.
/// </summary>
public ICollection<string> Files { get; } = new List<string>();
/// <summary>
/// Gets or sets the pipe name for receiving file list.
/// </summary>
public string PipeName { get; set; }
/// <summary>
/// Gets parse/validation errors produced by System.CommandLine.
/// </summary>
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
/// <summary>
/// Converts a boolean value to nullable bool (true -> true, false -> null).
/// </summary>
private static bool? ToBoolOrNull(bool value) => value ? true : null;
/// <summary>
/// Parses command-line arguments into CliOptions using System.CommandLine.
/// </summary>
/// <param name="args">The command-line arguments.</param>
/// <returns>A CliOptions instance with parsed values.</returns>
public static CliOptions Parse(string[] args)
{
var options = new CliOptions();
var cmd = new ImageResizerRootCommand();
// Parse using System.CommandLine
var parseResult = new Parser(cmd).Parse(args);
if (parseResult.Errors.Count > 0)
{
var errors = new List<string>(parseResult.Errors.Count);
foreach (var error in parseResult.Errors)
{
errors.Add(error.Message);
}
options.ParseErrors = new ReadOnlyCollection<string>(errors);
}
// Extract values from parse result using strongly typed options
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
options.Width = parseResult.GetValueForOption(cmd.WidthOption);
options.Height = parseResult.GetValueForOption(cmd.HeightOption);
options.Unit = parseResult.GetValueForOption(cmd.UnitOption);
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
// Convert bool to nullable bool (true -> true, false -> null)
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
options.RemoveMetadata = ToBoolOrNull(parseResult.GetValueForOption(cmd.RemoveMetadataOption));
options.KeepDateModified = ToBoolOrNull(parseResult.GetValueForOption(cmd.KeepDateModifiedOption));
options.ProgressLines = ToBoolOrNull(parseResult.GetValueForOption(cmd.ProgressLinesOption));
options.JpegQualityLevel = parseResult.GetValueForOption(cmd.QualityOption);
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
// Get files from arguments
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
if (files != null)
{
const string pipeNamePrefix = "\\\\.\\pipe\\";
foreach (var file in files)
{
// Check for pipe name (must be at the start of the path)
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
{
options.PipeName = file.Substring(pipeNamePrefix.Length);
}
else
{
options.Files.Add(file);
}
}
}
return options;
}
/// <summary>
/// Prints current configuration to the console.
/// </summary>
/// <param name="settings">The settings to display.</param>
public static void PrintConfig(ImageResizer.Properties.Settings settings)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigCustomSize);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigWidth, settings.CustomSize.Width, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigHeight, settings.CustomSize.Height, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigPresetSizes);
for (int i = 0; i < settings.Sizes.Count; i++)
{
var size = settings.Sizes[i];
var selected = i == settings.SelectedSizeIndex ? "*" : " ";
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPresetSizeFormat, i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
}
if (settings.SelectedSizeIndex >= settings.Sizes.Count)
{
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigCustomSelected, settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
}
}
/// <summary>
/// Prints usage information to the console.
/// </summary>
public static void PrintUsage()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
Console.WriteLine();
var cmd = new ImageResizerRootCommand();
// Print usage line
Console.WriteLine(Properties.Resources.CLI_UsageLine);
Console.WriteLine();
// Print options from the command definition
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
foreach (var option in cmd.Options)
{
var aliases = string.Join(", ", option.Aliases);
var description = option.Description ?? string.Empty;
Console.WriteLine($" {aliases,-30} {description}");
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
}
}
}

View File

@@ -10,60 +10,108 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ImageResizer.Properties;
using ImageResizer.Services;
namespace ImageResizer.Models
{
public class ResizeBatch
{
private readonly IFileSystem _fileSystem = new FileSystem();
private static IAISuperResolutionService _aiSuperResolutionService;
public string DestinationDirectory { get; set; }
public ICollection<string> Files { get; } = new List<string>();
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
public static void SetAiSuperResolutionService(IAISuperResolutionService service)
{
var batch = new ResizeBatch();
const string pipeNamePrefix = "\\\\.\\pipe\\";
string pipeName = null;
_aiSuperResolutionService = service;
}
for (var i = 0; i < args?.Length; i++)
public static void DisposeAiSuperResolutionService()
{
_aiSuperResolutionService?.Dispose();
_aiSuperResolutionService = null;
}
/// <summary>
/// Validates if a file path is a supported image format.
/// </summary>
/// <param name="path">The file path to validate.</param>
/// <returns>True if the path is valid and points to a supported image file.</returns>
private static bool IsValidImagePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
if (args[i] == "/d")
{
batch.DestinationDirectory = args[++i];
continue;
}
else if (args[i].Contains(pipeNamePrefix))
{
pipeName = args[i].Substring(pipeNamePrefix.Length);
continue;
}
batch.Files.Add(args[i]);
return false;
}
if (string.IsNullOrEmpty(pipeName))
if (!File.Exists(path))
{
return false;
}
var ext = Path.GetExtension(path)?.ToLowerInvariant();
var validExtensions = new[]
{
".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg",
".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp",
};
return validExtensions.Contains(ext);
}
/// <summary>
/// Creates a ResizeBatch from CliOptions.
/// </summary>
/// <param name="standardInput">Standard input stream for reading additional file paths.</param>
/// <param name="options">The parsed CLI options.</param>
/// <returns>A ResizeBatch instance.</returns>
public static ResizeBatch FromCliOptions(TextReader standardInput, CliOptions options)
{
var batch = new ResizeBatch
{
DestinationDirectory = options.DestinationDirectory,
};
foreach (var file in options.Files)
{
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
}
if (string.IsNullOrEmpty(options.PipeName))
{
// NB: We read these from stdin since there are limits on the number of args you can have
// Only read from stdin if it's redirected (piped input), not from interactive terminal
string file;
if (standardInput != null)
if (standardInput != null && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In)))
{
while ((file = standardInput.ReadLine()) != null)
{
batch.Files.Add(file);
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
}
}
}
else
{
using (NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", pipeName, PipeDirection.In))
new NamedPipeClientStream(".", options.PipeName, PipeDirection.In))
{
// Connect to the pipe or wait until the pipe is available.
pipeClient.Connect();
@@ -75,7 +123,10 @@ namespace ImageResizer.Models
// Display the read text to the console
while ((file = sr.ReadLine()) != null)
{
batch.Files.Add(file);
if (IsValidImagePath(file))
{
batch.Files.Add(file);
}
}
}
}
@@ -84,17 +135,26 @@ namespace ImageResizer.Models
return batch;
}
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
{
var options = CliOptions.Parse(args);
return FromCliOptions(standardInput, options);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
{
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
return Process(reportProgress, Settings.Default, cancellationToken);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
{
double total = Files.Count;
int completed = 0;
var errors = new ConcurrentBag<ResizeError>();
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
var settings = Settings.Default;
// TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
// APIs and a custom SynchronizationContext
Parallel.ForEach(
@@ -122,6 +182,9 @@ namespace ImageResizer.Models
}
protected virtual void Execute(string file, Settings settings)
=> new ResizeOperation(file, DestinationDirectory, settings).Execute();
{
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
}
}
}

View File

@@ -10,12 +10,14 @@ using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using ImageResizer.Extensions;
using ImageResizer.Properties;
using ImageResizer.Services;
using ImageResizer.Utilities;
using Microsoft.VisualBasic.FileIO;
@@ -30,6 +32,10 @@ namespace ImageResizer.Models
private readonly string _file;
private readonly string _destinationDirectory;
private readonly Settings _settings;
private readonly IAISuperResolutionService _aiSuperResolutionService;
// Cache CompositeFormat for AI error message formatting (CA1863)
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
private static readonly string[] _avoidFilenames =
@@ -39,11 +45,12 @@ namespace ImageResizer.Models
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
};
public ResizeOperation(string file, string destinationDirectory, Settings settings)
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
{
_file = file;
_destinationDirectory = destinationDirectory;
_settings = settings;
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
}
public void Execute()
@@ -167,6 +174,11 @@ namespace ImageResizer.Models
private BitmapSource Transform(BitmapSource source)
{
if (_settings.SelectedSize is AiSize)
{
return TransformWithAi(source);
}
int originalWidth = source.PixelWidth;
int originalHeight = source.PixelHeight;
@@ -257,6 +269,31 @@ namespace ImageResizer.Models
return scaledBitmap;
}
private BitmapSource TransformWithAi(BitmapSource source)
{
try
{
var result = _aiSuperResolutionService.ApplySuperResolution(
source,
_settings.AiSize.Scale,
_file);
if (result == null)
{
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
}
return result;
}
catch (Exception ex)
{
// Wrap the exception with a localized message
// This will be caught by ResizeBatch.Process() and displayed to the user
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
throw new InvalidOperationException(errorMessage, ex);
}
}
/// <summary>
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
/// In case of errors, we try to rebuild the metadata object and check again.
@@ -363,19 +400,24 @@ namespace ImageResizer.Models
}
// Remove directory characters from the size's name.
string sizeNameSanitized = _settings.SelectedSize.Name;
sizeNameSanitized = sizeNameSanitized
// For AI Size, use the scale display (e.g., "2×") instead of the full name
string sizeName = _settings.SelectedSize is AiSize aiSize
? aiSize.ScaleDisplay
: _settings.SelectedSize.Name;
string sizeNameSanitized = sizeName
.Replace('\\', '_')
.Replace('/', '_');
// Using CurrentCulture since this is user facing
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
var fileName = string.Format(
CultureInfo.CurrentCulture,
_settings.FileNameFormat,
originalFileName,
sizeNameSanitized,
_settings.SelectedSize.Width,
_settings.SelectedSize.Height,
selectedWidth,
selectedHeight,
encoder.Frames[0].PixelWidth,
encoder.Frames[0].PixelHeight);

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