Compare commits

..

34 Commits

Author SHA1 Message Date
Gleb Khmyznikov
d88930b5e3 simplify pipeline for debug 2025-08-29 09:26:42 +02:00
Gleb Khmyznikov
b1db93d97b merge 2025-08-27 18:07:32 +02:00
Gleb Khmyznikov
40c61c92bb Change pool parameter format in pipeline template 2025-08-26 15:22:32 +02:00
Gleb Khmyznikov
632b2f7fb9 Pull ScreenRuler tests 2025-08-26 15:11:49 +02:00
Gleb Khmyznikov
d21446b0f7 Update pipeline-ui-tests-automation.yml for Azure Pipelines 2025-08-26 15:05:56 +02:00
Gleb Khmyznikov
a9b97b1f8a Add pool parameter to UI tests pipeline template 2025-08-26 15:02:38 +02:00
Gleb Khmyznikov
1d0ef233c9 Add pool parameter to pipeline UI tests template 2025-08-26 15:01:27 +02:00
Gleb Khmyznikov
12c84b19c9 Merge branch 'main' into gleb/screen-ruler-uitests 2025-08-20 11:09:58 +02:00
Gleb Khmyznikov
501ff5690d minor addition to docs 2025-08-18 16:43:42 +02:00
Gleb Khmyznikov
38acf1a6bc disable some window attach methods 2025-08-11 18:45:50 +02:00
Gleb Khmyznikov
cb70e1dbfd Merge branch 'main' into gleb/screen-ruler-uitests 2025-08-11 10:16:54 +02:00
Gleb Khmyznikov
df750394db Filter EN symbols 2025-08-11 10:16:09 +02:00
Gleb Khmyznikov
9bcba8cb8b SendKey space 2025-08-07 18:36:28 +02:00
Gleb Khmyznikov
00457b2933 toggleSwitch to Toggle 2025-08-07 17:15:31 +02:00
Gleb Khmyznikov
2b0b80c63a add attempts 2025-08-07 16:29:09 +02:00
Gleb Khmyznikov
d42df8392c Trying to fix toggleClick 2025-08-07 15:35:01 +02:00
Gleb Khmyznikov
c6033bc8e5 change a bit the toggler click 2025-08-07 11:57:26 +02:00
Gleb Khmyznikov
69c206e85e increase more delays 2025-08-06 21:29:22 +02:00
Gleb Khmyznikov
0afb91b71d increase some delays 2025-08-06 19:52:59 +02:00
Gleb Khmyznikov
f926a5ece9 fix xaml format 2025-08-06 14:09:46 +02:00
Gleb Khmyznikov
b9e66cca26 Add big delays 2025-08-06 13:46:45 +02:00
Gleb Khmyznikov
c4dd213712 fix assembly name 2025-08-06 13:13:09 +02:00
Gleb Khmyznikov
2808f65d7f Merge branch 'main' into gleb/screen-ruler-uitests 2025-08-06 12:58:40 +02:00
Gleb Khmyznikov
0e61b28571 update solution name 2025-08-06 12:56:49 +02:00
Gleb Khmyznikov
ce406892b3 update the ruler test project name 2025-08-06 12:08:23 +02:00
Gleb Khmyznikov
f619334ff2 delete comment 2025-08-06 09:10:19 +02:00
Gleb Khmyznikov
142f09459d Add more tests and make the helper more straightforward 2025-08-05 19:43:16 +02:00
Gleb Khmyznikov
f42115bc4f Bounds tool test 2025-08-05 17:05:23 +02:00
Gleb Khmyznikov
36764487c5 Enabling screen ruler tests 2025-08-05 15:59:41 +02:00
Gleb Khmyznikov
218cb473c1 Basic complex test working 2025-08-04 18:19:04 +02:00
Gleb Khmyznikov
f0b674763a Use AutomationId instead of Name 2025-08-04 16:34:52 +02:00
Gleb Khmyznikov
cf0264e9b9 Add module toggle test 2025-08-04 12:06:18 +02:00
Gleb Khmyznikov
be7098c267 First simple test 2025-08-01 17:54:22 +02:00
Gleb Khmyznikov
3f486ea3db init 2025-08-01 10:39:36 +02:00
161 changed files with 1789 additions and 3395 deletions

View File

@@ -49,7 +49,6 @@ ALPHATYPE
AModifier
amr
ANDSCANS
animatedvisuals
Animnate
ANull
AOC
@@ -70,7 +69,6 @@ APPMODEL
APPNAME
appref
appsettings
appsfeatures
appwindow
appwiz
appxpackage
@@ -1026,6 +1024,8 @@ MYICON
NAMECHANGE
namespaceanddescendants
nao
Navigatable
NavigatablePage
NCACTIVATE
ncc
NCCALCSIZE
@@ -1316,7 +1316,6 @@ PRODUCTVERSION
Progman
programdata
projectname
projitems
PROPERTYKEY
Propset
PROPVARIANT
@@ -1396,7 +1395,6 @@ regkey
regroot
regsvr
REINSTALLMODE
releaseblog
reloadable
Relogger
remappings

View File

@@ -260,7 +260,3 @@ Process Process
# ZoomIt menu items with accelerator keys
E&xit
St&yle
# This matches a relative clause where the relative pronoun "that" is omitted.
# Example: "Gets or sets the window the TitleBar should configure."
\bthe\s+\w+\s+the\b

View File

@@ -1,20 +0,0 @@
name: Manual Batch Issue Deduplication
on:
workflow_dispatch: # Only runs when manually triggered
jobs:
batch-deduplicate:
runs-on: ubuntu-latest
steps:
- name: Batch Deduplicate Issues
uses: pelikhan/action-genai-issue-dedup@v0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
label-duplicate: "potential duplicate"
comment-duplicate: true
close-duplicate: false
batch-size: 100
since: '2019-05-05T00:00:00Z' # Process issues dating back to 2019
duplicate-comment-template: "This issue appears to be a duplicate of #{duplicate_issue_number}."
# Add other action-specific inputs if needed

View File

@@ -46,20 +46,7 @@ jobs:
SrcPath: $(Build.Repository.LocalPath)
TestArtifactsName: build-${{ variables.BuildPlatform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
${{ if ne(parameters.platform, 'ARM64') }}:
name: SHINE-INT-Testing-x64
${{ if eq(parameters.platform, 'x64Win11') }}:
demands: ImageOverride -equals SHINE-W11-Testing
${{ else }}:
name: SHINE-INT-Testing-arm64
${{ else }}:
${{ if ne(parameters.platform, 'ARM64') }}:
name: SHINE-OSS-Testing-x64
${{ if eq(parameters.platform, 'x64Win11') }}:
demands: ImageOverride -equals SHINE-W11-Testing
${{ else }}:
name: SHINE-OSS-Testing-arm64
name: testing-arm64-selfhost
steps:
- checkout: self
submodules: false
@@ -67,41 +54,41 @@ jobs:
fetchDepth: 1
fetchTags: false
- ${{ if eq(parameters.useLatestWebView2, true) }}:
- powershell: |
$edge_url = 'https://go.microsoft.com/fwlink/?linkid=2084649&Channel=Canary&language=en'
$timeout = New-TimeSpan -Minutes 6
$timeoutSeconds = [int]$timeout.TotalSeconds
$command = {
Invoke-WebRequest -Uri $using:edge_url -OutFile $(Pipeline.Workspace)\MicrosoftEdgeSetup.exe
Write-Host "##[command]Installing Canary channel of Microsoft Edge"
Start-Process $(Pipeline.Workspace)\MicrosoftEdgeSetup.exe -ArgumentList '/silent /install' -Wait
}
# - ${{ if eq(parameters.useLatestWebView2, true) }}:
# - powershell: |
# $edge_url = 'https://go.microsoft.com/fwlink/?linkid=2084649&Channel=Canary&language=en'
# $timeout = New-TimeSpan -Minutes 6
# $timeoutSeconds = [int]$timeout.TotalSeconds
# $command = {
# Invoke-WebRequest -Uri $using:edge_url -OutFile $(Pipeline.Workspace)\MicrosoftEdgeSetup.exe
# Write-Host "##[command]Installing Canary channel of Microsoft Edge"
# Start-Process $(Pipeline.Workspace)\MicrosoftEdgeSetup.exe -ArgumentList '/silent /install' -Wait
# }
$job = Start-Job -ScriptBlock $command
Wait-Job $job -Timeout $timeoutSeconds
if ($job.State -eq "Running") {
Stop-Job $job
Write-Host "##[warning]The job was stopped because it exceeded the time limit."
}
displayName: "Install the latest MSEdge Canary"
# $job = Start-Job -ScriptBlock $command
# Wait-Job $job -Timeout $timeoutSeconds
# if ($job.State -eq "Running") {
# Stop-Job $job
# Write-Host "##[warning]The job was stopped because it exceeded the time limit."
# }
# displayName: "Install the latest MSEdge Canary"
- script:
reg add "HKLM\Software\Policies\Microsoft\Edge\WebView2\ReleaseChannels" /v PowerToys.exe /t REG_SZ /d "3"
displayName: "Enable WebView2 Canary Channel"
# - script:
# reg add "HKLM\Software\Policies\Microsoft\Edge\WebView2\ReleaseChannels" /v PowerToys.exe /t REG_SZ /d "3"
# displayName: "Enable WebView2 Canary Channel"
- ${{ if ne(parameters.platform, 'arm64') }}:
- download: current
displayName: Download artifacts
artifact: $(TestArtifactsName)
patterns: |-
**
!**\*.pdb
!**\*.lib
- ${{ else }}:
- template: steps-download-artifacts-with-azure-cli.yml
parameters:
artifactName: $(TestArtifactsName)
# - ${{ if ne(parameters.platform, 'arm64') }}:
# - download: current
# displayName: Download artifacts
# artifact: $(TestArtifactsName)
# patterns: |-
# **
# !**\*.pdb
# !**\*.lib
# - ${{ else }}:
# - template: steps-download-artifacts-with-azure-cli.yml
# parameters:
# artifactName: $(TestArtifactsName)
- template: steps-ensure-dotnet-version.yml
parameters:
@@ -111,9 +98,9 @@ jobs:
- task: VisualStudioTestPlatformInstaller@1
displayName: Ensure VSTest Platform
- pwsh: |-
& '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1'
displayName: Download and install WinAppDriver
# - pwsh: |-
# & '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1'
# displayName: Download and install WinAppDriver
- ${{ if ne(parameters.buildSource, 'buildNow') }}:
- task: DownloadPipelineArtifact@2

View File

@@ -34,6 +34,9 @@ parameters:
- name: uiTestModules
type: object
default: []
- name: pool
type: string
default: 'SHINE-OSS-Testing-arm64-selfhost'
stages:
- ${{ each platform in parameters.buildPlatforms }}:
@@ -46,6 +49,7 @@ stages:
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
uiTestModules: ${{ parameters.uiTestModules }}
pool: ${{ parameters.pool }}
# Official build path: build UI tests only + download official build + run tests
- ${{ if ne(parameters.buildSource, 'buildNow') }}:

View File

@@ -14,6 +14,8 @@ parameters:
- name: uiTestModules
type: object
default: []
- name: pool
type: string
stages:
# Stage 1: Build full PowerToys project
@@ -23,13 +25,7 @@ stages:
jobs:
- template: job-build-project.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
pool: [ ${{ parameters.pool }} ]
buildPlatforms:
- ${{ parameters.platform }}
buildConfigurations: [Release]

View File

@@ -23,6 +23,7 @@
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250703-build.2173" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.TitleBar" Version="0.0.1-build.2206" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

@@ -1499,6 +1499,7 @@ SOFTWARE.
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
- CommunityToolkit.Labs.WinUI.TitleBar
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections

View File

@@ -793,6 +793,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"

199
README.md
View File

@@ -35,19 +35,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] |
This is our preferred method.
@@ -93,145 +93,118 @@ For guidance on developing for PowerToys, please read the [developer docs](./doc
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.94 - Sep 2025 Update
### 0.93 - Aug 2025 Update
In this release, we focused on new features, stability, optimization improvements, and automation.
For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
**✨Highlights**
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for singlebutton cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
- Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
### Always On Top
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience.
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run.
- Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK.
- Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)!
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations.
- Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage.
### Command Palette
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Standardized null checks using C# pattern matching for safer behavior.
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
- Added single-select filters to DynamicListPage and updated Windows Services sample.
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Propagated alias changes safely and resolved conflicts across view models.
- Allowed providers to override Dispose with a virtual method.
- Fixed memory leaks by cleaning up removed or cancelled list items.
- Sorted DateTime extension results by relevance for better usability.
- Reduced search text “jiggling” by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- Added “evil” sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
- Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved UI design with better text sizing and alignment.
- Fixed keyboard shortcuts to work better in text boxes and context menus.
- Added right-click context menus with critical command styling and separators.
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality.
- Fixed context menu crashes with better type handling.
- Fixed "Reload" command to work with both uppercase and lowercase letters.
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window focus not returning to previous app properly.
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)!
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
### Command Palette extensions
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added app icons to the All Apps "Run" context command when available.
- Restored missing builtin icons by standardizing extension dependencies.
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
### Hosts File Editor
- Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
### Image Resizer
- Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
- Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature.
- Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs).
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)!
- Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
- Added keyboard shortcuts to the *Apps* context menu items for faster access.
- Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality.
- Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer.
- Added command history to the *Run* page for easier access to previous commands.
- Fixed directory path handling in *Run* fallback for better file navigation.
- Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added fallback command to *Windows Settings* extension for better search results.
- Re-enabled *Clipboard History* feature with proper window handling.
- Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input.
- Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows.
- Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)!
### Mouse Utilities
- Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
### Mouse Without Borders
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
- Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen.
### Peek
- Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
### PowerRename
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
- Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)!
### Quick Accent
- Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
- Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
### Settings
- Added telemetry to track usage of the new shortcut conflict detection workflow.
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
- Introduced Settings search with fuzzy matching and navigation to specific controls.
- Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
- Localized conflict messages in the conflict window and dialog.
### Installer
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
- Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list.
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand.
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing.
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings.
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window.
- Resolved a settings crash related to incompatible property names in ZoomIt configuration.
### Documentation
- Adds docs for building the installer locally and testing winget installs.
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
- Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)!
- Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)!
- Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)!
### Development
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Added accessibility IDs to CmdPal UI for stable UI tests.
- Rewrote system command tests with a new test base and cleaner patterns.
- Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- Cleans up AIgenerated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)!
- Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)!
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother.
- Replaced NuGet feed with Azure Artifacts for better package management.
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
- Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts.
- Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck.
- Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)!
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality.
- Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches.
### What is being planned over the next few releases
For [v0.95][github-next-release-work], we'll work on the items below:
For [v0.94][github-next-release-work], we'll work on the items below:
- Continued Command Palette polish
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
- Working on upgrading the installer to WiX 5
- Working on shortcut conflict detection
- Working on setting search
- Upgrading Keyboard Manager's editor UI
- UI tweaking utility with day/night theme switcher
- DSC v3 support for top utilities
- New UI automation tests
- Stability, bug fixes

View File

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

View File

@@ -32,8 +32,17 @@ namespace ManagedCommon
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
{
string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
string basePath = Path.GetDirectoryName(versionedPath);
string basePath;
if (isLocalLow)
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
if (!Directory.Exists(versionedPath))
{
@@ -50,22 +59,6 @@ namespace ManagedCommon
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
}
public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false)
{
string basePath;
if (isLocalLow)
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
return versionedPath;
}
/// <summary>
/// Deletes old version log folders, keeping only the current version's folder.
/// </summary>
@@ -122,13 +115,13 @@ namespace ManagedCommon
{
var exMessage =
message + Environment.NewLine +
ex.GetType() + " (" + ex.HResult + "): " + ex.Message + Environment.NewLine;
ex.GetType() + ": " + ex.Message + Environment.NewLine;
if (ex.InnerException != null)
{
exMessage +=
"Inner exception: " + Environment.NewLine +
ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
ex.InnerException.GetType() + ": " + ex.InnerException.Message + Environment.NewLine;
}
exMessage +=

View File

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

View File

@@ -20,14 +20,27 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/EnvironmentVariables/EnvironmentVariables.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<Grid
x:Name="titleBar"
Height="32"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/EnvironmentVariables/EnvironmentVariables.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
</Grid>
</winuiex:WindowEx>

View File

@@ -4,19 +4,22 @@
using System;
using System.Runtime.InteropServices;
using EnvironmentVariables.Win32;
using EnvironmentVariablesUILib;
using EnvironmentVariablesUILib.Helpers;
using EnvironmentVariablesUILib.ViewModels;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
namespace EnvironmentVariables
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
private EnvironmentVariablesMainPage MainPage { get; }
@@ -31,9 +34,8 @@ namespace EnvironmentVariables
AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
var loader = ResourceLoaderInstance.ResourceLoader;
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
Title = title;
titleBar.Title = title;
AppTitleTextBlock.Text = title;
var handle = this.GetWindowHandle();
RegisterWindow(handle);

View File

@@ -19,26 +19,6 @@
class FileLocksmithModule : public PowertoyModuleIface
{
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
Logger::info(L"File Locksmith context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered");
#endif
}
}
public:
FileLocksmithModule()
{
@@ -108,16 +88,21 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
#endif
m_enabled = true;
UpdateRegistration(m_enabled);
}
virtual void disable() override
{
Logger::info(L"File Locksmith disabled");
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered (Win10)");
#endif
m_enabled = false;
UpdateRegistration(m_enabled);
}
virtual bool is_enabled() override
@@ -150,7 +135,6 @@ private:
{
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
UpdateRegistration(m_enabled);
Trace::EnableFileLocksmith(m_enabled);
}

View File

@@ -20,15 +20,30 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/FileLocksmith/Icon.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<Grid
x:Name="AppTitleBar"
Height="32"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightDragColumn" Width="*" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/FileLocksmith/Icon.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
<views:MainPage x:Name="mainPage" Grid.Row="1" />
</Grid>
</winuiex:WindowEx>
</winuiex:WindowEx>

View File

@@ -18,16 +18,30 @@ namespace FileLocksmithUI
{
InitializeComponent();
mainPage.ViewModel.IsElevated = isElevated;
SetTitleBar(titleBar);
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
SetTitleBar(AppTitleBar);
Activated += MainWindow_Activated;
AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico");
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
var loader = ResourceLoaderInstance.ResourceLoader;
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
Title = title;
titleBar.Title = title;
AppTitleTextBlock.Text = title;
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
AppTitleTextBlock.Foreground =
(SolidColorBrush)App.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
AppTitleTextBlock.Foreground =
(SolidColorBrush)App.Current.Resources["WindowCaptionForeground"];
}
}
public void Dispose()

View File

@@ -190,7 +190,7 @@
TextWrapping="Wrap" />
</ContentDialog>
<ContentDialog x:Name="ProcessFilesListDialog" x:Uid="ProcessFilesListDialog">
<ScrollViewer Padding="16" HorizontalScrollBarVisibility="Auto">
<ScrollViewer Padding="15" HorizontalScrollBarVisibility="Auto">
<TextBlock
x:Name="ProcessFilesListDialogTextBlock"
x:Uid="ProcessFilesListDialogTextBlock"

View File

@@ -20,14 +20,27 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/Hosts/Hosts.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<Grid
x:Name="titleBar"
Height="32"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/Hosts/Hosts.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
</Grid>
</winuiex:WindowEx>

View File

@@ -9,15 +9,19 @@ using HostsUILib.Helpers;
using HostsUILib.Views;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.ApplicationModel.Resources;
using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Hosts
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
private HostsMainPage MainPage { get; }
@@ -34,18 +38,31 @@ namespace Hosts
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
Title = title;
titleBar.Title = title;
AppTitleTextBlock.Text = title;
var handle = this.GetWindowHandle();
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(handle);
WindowHelpers.BringToForeground(handle);
Activated += MainWindow_Activated;
MainPage = Host.GetService<HostsMainPage>();
PowerToysTelemetry.Log.WriteEvent(new HostEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
AppTitleTextBlock.Foreground = (SolidColorBrush)App.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
AppTitleTextBlock.Foreground = (SolidColorBrush)App.Current.Resources["WindowCaptionForeground"];
}
}
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
MainGrid.Children.Add(MainPage);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestBounds : UITestBase
{
public TestBounds()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.BoundsTool")]
[TestCategory("Spacing")]
public void TestScreenRulerBoundsTool()
{
TestHelper.InitializeTest(this, "bounds test");
TestHelper.PerformBoundsToolTest(this);
TestHelper.CleanupTest(this);
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestSpacing : UITestBase
{
public TestSpacing()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.SpacingTool")]
[TestCategory("Spacing")]
public void TestScreenRulerSpacingTool()
{
TestHelper.InitializeTest(this, "spacing test");
TestHelper.PerformSpacingToolTest(this, TestHelper.SpacingButtonName, "Spacing");
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestSpacingHorizontal : UITestBase
{
public TestSpacingHorizontal()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.HorizontalSpacingTool")]
[TestCategory("Spacing")]
public void TestScreenRulerHorizontalSpacingTool()
{
TestHelper.InitializeTest(this, "horizontal spacing test");
TestHelper.PerformSpacingToolTest(this, TestHelper.HorizontalSpacingButtonName, "Horizontal Spacing");
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ScreenRuler.UITests
{
[TestClass]
public class TestSpacingVertical : UITestBase
{
public TestSpacingVertical()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("ScreenRuler.VerticalSpacingTool")]
[TestCategory("Spacing")]
public void TestScreenRulerVerticalSpacingTool()
{
TestHelper.InitializeTest(this, "vertical spacing test");
TestHelper.PerformSpacingToolTest(this, TestHelper.VerticalSpacingButtonName, "Vertical Spacing");
TestHelper.CleanupTest(this);
}
}
}

View File

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

View File

@@ -21,26 +21,6 @@
// Note: Settings are managed via Settings and UI Settings
class NewModule : public PowertoyModuleIface
{
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
Logger::info(L"New+ context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered");
#endif
}
}
public:
NewModule()
{
@@ -118,9 +98,14 @@ public:
{
newplus::utilities::register_msix_package();
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
#endif
}
powertoy_new_enabled = true;
UpdateRegistration(powertoy_new_enabled);
}
virtual void disable() override
@@ -165,14 +150,19 @@ private:
{
Trace::EventToggleOnOff(false);
}
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered (Win10)");
#endif
}
powertoy_new_enabled = false;
UpdateRegistration(powertoy_new_enabled);
}
void init_settings()
{
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
UpdateRegistration(powertoy_new_enabled);
}
};

View File

@@ -49,9 +49,7 @@ namespace Awake.Core
private static DateTimeOffset ExpireAt { get; set; }
private static readonly CompositeFormat AwakeMinute = CompositeFormat.Parse(Resources.AWAKE_MINUTE);
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
private static readonly CompositeFormat AwakeHour = CompositeFormat.Parse(Resources.AWAKE_HOUR);
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
@@ -453,7 +451,7 @@ namespace Awake.Core
Dictionary<string, uint> optionsList = new()
{
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHour, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
};
return optionsList;

View File

@@ -159,15 +159,6 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} hour.
/// </summary>
internal static string AWAKE_HOUR {
get {
return ResourceManager.GetString("AWAKE_HOUR", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} hours.
/// </summary>
@@ -249,15 +240,6 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} minute.
/// </summary>
internal static string AWAKE_MINUTE {
get {
return ResourceManager.GetString("AWAKE_MINUTE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} minutes.
/// </summary>

View File

@@ -123,10 +123,6 @@
<data name="AWAKE_EXIT" xml:space="preserve">
<value>Exit</value>
</data>
<data name="AWAKE_HOUR" xml:space="preserve">
<value>{0} hour</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_HOURS" xml:space="preserve">
<value>{0} hours</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@@ -146,10 +142,6 @@
<value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment>
</data>
<data name="AWAKE_MINUTE" xml:space="preserve">
<value>{0} minute</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_MINUTES" xml:space="preserve">
<value>{0} minutes</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>

View File

@@ -160,7 +160,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Initialized |= InitializedState.Initialized;
}
public virtual void SlowInitializeProperties()
public void SlowInitializeProperties()
{
if (IsSelectedInitialized)
{

View File

@@ -47,21 +47,9 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateTags(li.Tags);
TextToSuggest = li.TextToSuggest;
Section = li.Section ?? string.Empty;
UpdateProperty(nameof(Section));
}
public override void SlowInitializeProperties()
{
base.SlowInitializeProperties();
var model = Model.Unsafe;
if (model is null)
{
return;
}
var extensionDetails = model.Details;
var extensionDetails = li.Details;
if (extensionDetails is not null)
{
Details = new(extensionDetails, PageContext);
@@ -70,8 +58,8 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails));
}
TextToSuggest = model.TextToSuggest;
UpdateProperty(nameof(TextToSuggest));
UpdateProperty(nameof(Section));
}
protected override void FetchProperty(string propertyName)

View File

@@ -3,12 +3,10 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -41,7 +39,7 @@ public partial class AppStateModel : ObservableObject
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadState)}");
}
if (!File.Exists(FilePath))
@@ -79,84 +77,43 @@ public partial class AppStateModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
// validate JSON
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
{
Logger.LogError("Failed to parse app state as a JsonObject.");
return;
}
// Now, read the existing content from the file
var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}";
// read previous settings
if (!TryReadSavedState(out var savedSettings))
{
savedSettings = new JsonObject();
}
// Is it valid JSON?
if (JsonNode.Parse(oldContent) is JsonObject savedSettings)
{
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
// merge new settings into old ones
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and
// reload the settings then
model.StateChanged?.Invoke(model, null);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
}
}
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
{
savedSettings = null;
// read existing content from the file
string oldContent;
try
{
if (File.Exists(FilePath))
{
oldContent = File.ReadAllText(FilePath);
// TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and
// reload the settings then
model.StateChanged?.Invoke(model, null);
}
else
{
Debug.WriteLine("Failed to parse settings file as JsonObject.");
}
}
else
{
// file doesn't exist (might not have been created yet), so consider this a success
// and return empty settings
savedSettings = new JsonObject();
return true;
Debug.WriteLine("Failed to parse settings file as JsonObject.");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
return false;
}
// detect empty file, just for sake of logging
if (string.IsNullOrWhiteSpace(oldContent))
{
Logger.LogInfo($"App state file is empty: {FilePath}");
return false;
}
// is it valid JSON?
try
{
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
return savedSettings != null;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
return false;
Debug.WriteLine(ex.ToString());
}
}

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -20,10 +19,6 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem
Title = string.Empty;
_logMessagesPage.Name = string.Empty;
Subtitle = Properties.Resources.builtin_log_subtitle;
var logPath = Logger.LogDirectoryPath("\\CmdPal\\Logs\\");
var openLogCommand = new OpenFileCommand(logPath) { Name = Resources.builtin_log_folder_command_name };
MoreCommands = [new CommandContextItem(openLogCommand)];
}
public override void UpdateQuery(string query)

View File

@@ -27,9 +27,7 @@ public partial class MainListPage : DynamicListPage,
private readonly IServiceProvider _serviceProvider;
private readonly TopLevelCommandManager _tlcManager;
private IEnumerable<Scored<IListItem>>? _filteredItems;
private IEnumerable<Scored<IListItem>>? _filteredApps;
private IEnumerable<IListItem>? _allApps;
private IEnumerable<IListItem>? _filteredItems;
private bool _includeApps;
private bool _filteredItemsIncludesApps;
@@ -85,7 +83,7 @@ public partial class MainListPage : DynamicListPage,
}
else
{
RaiseItemsChanged();
RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
}
}
@@ -150,13 +148,7 @@ public partial class MainListPage : DynamicListPage,
{
lock (_tlcManager.TopLevelCommands)
{
var items = Enumerable.Empty<Scored<IListItem>>()
.Concat(_filteredItems is not null ? _filteredItems : [])
.Concat(_filteredApps is not null ? _filteredApps : [])
.OrderByDescending(o => o.Score)
.Select(s => s.Item)
.ToArray();
return items;
return _filteredItems?.ToArray() ?? [];
}
}
}
@@ -175,8 +167,6 @@ public partial class MainListPage : DynamicListPage,
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
}
@@ -194,8 +184,6 @@ public partial class MainListPage : DynamicListPage,
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
_filteredApps = null;
_allApps = null;
RaiseItemsChanged(commands.Count);
return;
}
@@ -205,49 +193,35 @@ public partial class MainListPage : DynamicListPage,
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
{
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
// If the internal state has changed, reset _filteredItems to reset the list.
if (_filteredItemsIncludesApps != _includeApps)
{
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
var newFilteredItems = _filteredItems?.Select(s => s.Item);
// If we don't have any previous filter results to work with, start
// with a list of all our commands & apps.
if (newFilteredItems is null && _filteredApps is null)
if (_filteredItems is null)
{
newFilteredItems = commands;
_filteredItems = commands;
_filteredItemsIncludesApps = _includeApps;
if (_includeApps)
{
_allApps = AllAppsCommandProvider.Page.GetItems();
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
var appIds = apps.Select(app => app.Command.Id).ToArray();
// Remove any top level pinned apps and use the apps from AllAppsCommandProvider.Page.GetItems()
// since they contain details.
_filteredItems = _filteredItems.Where(item => item.Command is not AppCommand);
_filteredItems = _filteredItems.Concat(apps);
}
}
// Produce a list of everything that matches the current filter.
_filteredItems = ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem);
// Produce a list of filtered apps with the appropriate limit
if (_allApps is not null)
{
_filteredApps = ListHelpers.FilterListWithScores<IListItem>(_allApps, SearchText, ScoreTopLevelItem);
var appResultLimit = AllAppsCommandProvider.TopLevelResultLimit;
if (appResultLimit >= 0)
{
_filteredApps = _filteredApps.Take(appResultLimit);
}
}
RaiseItemsChanged();
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
RaiseItemsChanged(_filteredItems.Count());
}
}

View File

@@ -126,10 +126,6 @@ public class ExtensionWrapper : IExtensionWrapper
// We'll just return out nothing.
return;
}
else if (hr.Value != 0)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
}
// Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);

View File

@@ -285,15 +285,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to View log folder.
/// </summary>
public static string builtin_log_folder_command_name {
get {
return ResourceManager.GetString("builtin_log_folder_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to View log.
/// </summary>

View File

@@ -135,9 +135,6 @@
<data name="builtin_log_title" xml:space="preserve">
<value>View log</value>
</data>
<data name="builtin_log_folder_command_name" xml:space="preserve">
<value>View log folder</value>
</data>
<data name="builtin_reload_subtitle" xml:space="preserve">
<value>Reload Command Palette extensions</value>
</data>

View File

@@ -32,7 +32,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private string _generatedId = string.Empty;
private HotkeySettings? _hotkey;
private IIconInfo? _initialIcon;
private CommandAlias? Alias { get; set; }
@@ -58,8 +57,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
public IIconInfo Icon => _commandItemViewModel.Icon;
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
@@ -208,8 +205,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
DisplayTitle = fallback.DisplayTitle;
}
UpdateInitialIcon(false);
}
}
@@ -226,31 +221,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
FetchAliasFromAliasManager();
UpdateHotkey();
UpdateTags();
UpdateInitialIcon();
}
else if (e.PropertyName == nameof(CommandItem.Icon))
{
UpdateInitialIcon();
}
}
}
private void UpdateInitialIcon(bool raiseNotification = true)
{
if (_initialIcon != null || !_commandItemViewModel.Icon.IsSet)
{
return;
}
_initialIcon = _commandItemViewModel.Icon;
if (raiseNotification)
{
DoOnUiThread(
() =>
{
PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(InitialIcon)));
});
}
}

View File

@@ -125,7 +125,7 @@
Width="20"
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind InitialIcon, Mode=OneWay}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>

View File

@@ -438,7 +438,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="NavigationPaneClosed" xml:space="preserve">
<value>Navigation pane closed</value>
</data>
<data name="NavigationPaneOpened" xml:space="preserve">
<data name="NavigationPageOpened" xml:space="preserve">
<value>Navigation page opened</value>
</data>
</root>

View File

@@ -15,13 +15,13 @@ public class MockSettingsInterface : ISettingsInterface
public bool GlobalIfURI { get; set; }
public uint HistoryItemCount { get; set; }
public string ShowHistory { get; set; }
public MockSettingsInterface(uint historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
public MockSettingsInterface(string showHistory = "none", bool globalIfUri = true, List<HistoryItem> mockHistory = null)
{
_historyItems = mockHistory ?? new List<HistoryItem>();
GlobalIfURI = globalIfUri;
HistoryItemCount = historyItemCount;
ShowHistory = showHistory;
}
public List<ListItem> LoadHistory()
@@ -50,9 +50,9 @@ public class MockSettingsInterface : ISettingsInterface
_historyItems.Add(historyItem);
// Simulate the same logic as SettingsManager
if (HistoryItemCount > 0)
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
while (_historyItems.Count > HistoryItemCount)
while (_historyItems.Count > maxHistoryItems)
{
_historyItems.RemoveAt(0); // Remove the oldest item
}

View File

@@ -54,7 +54,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
var page = new WebSearchListPage(settings);
@@ -89,7 +89,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
var page = new WebSearchListPage(settings);
@@ -122,7 +122,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "None");
var page = new WebSearchListPage(settings);

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>5</VersionMinor>
<VersionMinor>4</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
@@ -43,28 +45,6 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
}
public static int TopLevelResultLimit
{
get
{
var limitSetting = AllAppsSettings.Instance.SearchResultLimit;
if (limitSetting is null)
{
return -1;
}
var quantity = -1;
if (int.TryParse(limitSetting, out var result))
{
quantity = result;
}
return quantity;
}
}
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)

View File

@@ -20,16 +20,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _searchResultLimitChoices =
[
new ChoiceSetSetting.Choice(Resources.limit_none, "-1"),
new ChoiceSetSetting.Choice(Resources.limit_0, "0"),
new ChoiceSetSetting.Choice(Resources.limit_1, "1"),
new ChoiceSetSetting.Choice(Resources.limit_5, "5"),
new ChoiceSetSetting.Choice(Resources.limit_10, "10"),
new ChoiceSetSetting.Choice(Resources.limit_20, "20"),
];
#pragma warning disable SA1401 // Fields should be private
internal static AllAppsSettings Instance = new();
#pragma warning restore SA1401 // Fields should be private
@@ -52,14 +42,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value;
private readonly ChoiceSetSetting _searchResultLimitSource = new(
Namespaced(nameof(SearchResultLimit)),
Resources.limit_fallback_results_source,
Resources.limit_fallback_results_source_description,
_searchResultLimitChoices);
public string SearchResultLimit => _searchResultLimitSource.Value ?? string.Empty;
private readonly ToggleSetting _enableStartMenuSource = new(
Namespaced(nameof(EnableStartMenuSource)),
Resources.enable_start_menu_source,
@@ -105,7 +87,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
Settings.Add(_enableDesktopSource);
Settings.Add(_enableRegistrySource);
Settings.Add(_enablePathEnvironmentVariableSource);
Settings.Add(_searchResultLimitSource);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -1,130 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Management.Deployment;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UninstallApplicationCommand : InvokableCommand
{
// This is a ms-settings URI that opens the Apps & Features page in Windows Settings.
// It's correct and follows the Microsoft documentation:
// https://learn.microsoft.com/en-us/windows/apps/develop/launch/launch-settings-app#apps
private const string AppsFeaturesUri = "ms-settings:appsfeatures";
private readonly UWPApplication? _uwpTarget;
private readonly Win32Program? _win32Target;
public UninstallApplicationCommand(UWPApplication target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public UninstallApplicationCommand(Win32Program target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
}
private async Task<CommandResult> UninstallUwpAppAsync(UWPApplication app)
{
if (string.IsNullOrWhiteSpace(app.Package.FullName))
{
Logger.LogError($"Critical error while uninstalling: packageFullName cannot be null or empty.");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
try
{
// Which timeout to use for the uninstallation operation?
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
{
var packageManager = new PackageManager();
var result = await packageManager.RemovePackageAsync(app.Package.FullName).AsTask(cts.Token);
if (result.ErrorText is not null && result.ErrorText.Length > 0)
{
Logger.LogError($"Failed to uninstall {app.Package.FullName}: {result.ErrorText}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
}
// TODO: Update the Search results after uninstalling the app - unsure how to do this yet.
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_successful), app.DisplayName),
Result = CommandResult.GoHome(),
});
}
catch (OperationCanceledException)
{
Logger.LogError($"Timeout exceeded while uninstalling {app.Package.FullName}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
catch (UnauthorizedAccessException ex)
{
Logger.LogError($"Permission denied to uninstall {app.Package.FullName}. Elevated privileges may be required. Error: {ex.Message}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
catch (Exception ex)
{
Logger.LogError($"An unexpected error occurred during uninstallation of {app.Package.FullName}: {ex.Message}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
}
public override CommandResult Invoke()
{
if (_uwpTarget is not null)
{
return UninstallUwpAppAsync(_uwpTarget).ConfigureAwait(false).GetAwaiter().GetResult();
}
if (_win32Target is not null)
{
Process.Start(new ProcessStartInfo
{
FileName = AppsFeaturesUri,
UseShellExecute = true,
});
return CommandResult.Dismiss();
}
Logger.LogError("UninstallApplicationCommand invoked with no target.");
return CommandResult.Dismiss();
}
}

View File

@@ -1,66 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Text;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UninstallApplicationConfirmation : InvokableCommand
{
private readonly UWPApplication? _uwpTarget;
private readonly Win32Program? _win32Target;
public UninstallApplicationConfirmation(UWPApplication target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public UninstallApplicationConfirmation(Win32Program target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
}
public override CommandResult Invoke()
{
UninstallApplicationCommand uninstallCommand;
var applicationTitle = Resources.uninstall_application;
if (_uwpTarget is not null)
{
uninstallCommand = new UninstallApplicationCommand(_uwpTarget);
applicationTitle = _uwpTarget.DisplayName;
}
else if (_win32Target is not null)
{
uninstallCommand = new UninstallApplicationCommand(_win32Target);
applicationTitle = _win32Target.Name;
}
else
{
Logger.LogError("UninstallApplicationCommand invoked with no target.");
return CommandResult.Dismiss();
}
var confirmArgs = new ConfirmationArgs()
{
Title = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_confirm_title), applicationTitle),
Description = Resources.uninstall_application_confirm_description,
PrimaryCommand = uninstallCommand,
IsPrimaryCommandCritical = true,
};
return CommandResult.Confirm(confirmArgs);
}
}

View File

@@ -21,6 +21,4 @@ internal sealed class Icons
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
}

View File

@@ -117,14 +117,6 @@ public class UWPApplication : IUWPApplication
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
});
commands.Add(
new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
return commands;
}

View File

@@ -219,16 +219,6 @@ public class Win32Program : IProgram
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
});
if (AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication || AppType == ApplicationType.Win32Application)
{
commands.Add(new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
}
return commands;
}

View File

@@ -159,78 +159,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to 0.
/// </summary>
internal static string limit_0 {
get {
return ResourceManager.GetString("limit_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1.
/// </summary>
internal static string limit_1 {
get {
return ResourceManager.GetString("limit_1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 10.
/// </summary>
internal static string limit_10 {
get {
return ResourceManager.GetString("limit_10", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 20.
/// </summary>
internal static string limit_20 {
get {
return ResourceManager.GetString("limit_20", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 5.
/// </summary>
internal static string limit_5 {
get {
return ResourceManager.GetString("limit_5", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit the number of applications returned from the top level.
/// </summary>
internal static string limit_fallback_results_source {
get {
return ResourceManager.GetString("limit_fallback_results_source", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit fallback results to n apps.
/// </summary>
internal static string limit_fallback_results_source_description {
get {
return ResourceManager.GetString("limit_fallback_results_source_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unlimited.
/// </summary>
internal static string limit_none {
get {
return ResourceManager.GetString("limit_none", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open containing folder.
/// </summary>
@@ -330,51 +258,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall.
/// </summary>
internal static string uninstall_application {
get {
return ResourceManager.GetString("uninstall_application", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This app and its related information will be removed..
/// </summary>
internal static string uninstall_application_confirm_description {
get {
return ResourceManager.GetString("uninstall_application_confirm_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall &quot;{0}&quot;?.
/// </summary>
internal static string uninstall_application_confirm_title {
get {
return ResourceManager.GetString("uninstall_application_confirm_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error while uninstalling &apos;{0}&apos;.
/// </summary>
internal static string uninstall_application_failed {
get {
return ResourceManager.GetString("uninstall_application_failed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;{0}&apos; has been successfully uninstalled..
/// </summary>
internal static string uninstall_application_successful {
get {
return ResourceManager.GetString("uninstall_application_successful", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unpin.
/// </summary>

View File

@@ -198,43 +198,4 @@
<data name="unpin_app" xml:space="preserve">
<value>Unpin</value>
</data>
<data name="uninstall_application" xml:space="preserve">
<value>Uninstall</value>
</data>
<data name="uninstall_application_successful" xml:space="preserve">
<value>'{0}' has been successfully uninstalled.</value>
</data>
<data name="uninstall_application_failed" xml:space="preserve">
<value>Error while uninstalling '{0}'</value>
</data>
<data name="uninstall_application_confirm_description" xml:space="preserve">
<value>This app and its related information will be removed.</value>
</data>
<data name="uninstall_application_confirm_title" xml:space="preserve">
<value>Uninstall "{0}"?</value>
</data>
<data name="limit_1" xml:space="preserve">
<value>1</value>
</data>
<data name="limit_5" xml:space="preserve">
<value>5</value>
</data>
<data name="limit_10" xml:space="preserve">
<value>10</value>
</data>
<data name="limit_20" xml:space="preserve">
<value>20</value>
</data>
<data name="limit_fallback_results_source" xml:space="preserve">
<value>Limit the number of applications returned from the top level</value>
</data>
<data name="limit_fallback_results_source_description" xml:space="preserve">
<value>Limit fallback results to n apps</value>
</data>
<data name="limit_0" xml:space="preserve">
<value>0</value>
</data>
<data name="limit_none" xml:space="preserve">
<value>Unlimited</value>
</data>
</root>
</root>

View File

@@ -17,7 +17,6 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
{
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.LockIcon;
var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi;
var hideEmptyRB = settings.HideEmptyRecycleBin();

View File

@@ -22,7 +22,6 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
{
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.TimeDateIcon;
_settingsManager = settings;
_timestamp = timestamp;

View File

@@ -79,22 +79,16 @@ public sealed partial class TimeDateCalculator
}
else
{
List<(int Score, AvailableResult Item)> itemScores = [];
// Generate filtered list of results
foreach (var f in availableFormats)
{
var score = f.Score(query, f.Label, f.AlternativeSearchTag);
if (score > 0)
{
itemScores.Add((score, f));
results.Add(f.ToListItem());
}
}
results = itemScores
.OrderByDescending(s => s.Score)
.Select(s => s.Item.ToListItem())
.ToList();
}
if (results.Count == 0)

View File

@@ -34,7 +34,7 @@ internal sealed partial class SearchWebCommand : InvokableCommand
return CommandResult.KeepOpen();
}
if (_settingsManager.HistoryItemCount != 0)
if (_settingsManager.ShowHistory != Resources.history_none)
{
_settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now));
}

View File

@@ -11,7 +11,7 @@ public interface ISettingsInterface
{
public bool GlobalIfURI { get; }
public uint HistoryItemCount { get; }
public string ShowHistory { get; }
public List<ListItem> LoadHistory();

View File

@@ -16,7 +16,6 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
private readonly string _historyPath;
private static readonly string _namespace = "websearch";
@@ -25,11 +24,11 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
private static readonly List<ChoiceSetSetting.Choice> _choices =
[
new ChoiceSetSetting.Choice(Resources.history_none, "None"),
new ChoiceSetSetting.Choice(Resources.history_1, "1"),
new ChoiceSetSetting.Choice(Resources.history_5, "5"),
new ChoiceSetSetting.Choice(Resources.history_10, "10"),
new ChoiceSetSetting.Choice(Resources.history_20, "20"),
new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none),
new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1),
new ChoiceSetSetting.Choice(Resources.history_5, Resources.history_5),
new ChoiceSetSetting.Choice(Resources.history_10, Resources.history_10),
new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20),
];
private readonly ToggleSetting _globalIfURI = new(
@@ -38,15 +37,15 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.plugin_global_if_uri,
false);
private readonly ChoiceSetSetting _historyItemCount = new(
Namespaced(HistoryItemCountLegacySettingsKey),
Resources.plugin_history_item_count,
Resources.plugin_history_item_count,
private readonly ChoiceSetSetting _showHistory = new(
Namespaced(nameof(ShowHistory)),
Resources.plugin_show_history,
Resources.plugin_show_history,
_choices);
public bool GlobalIfURI => _globalIfURI.Value;
public uint HistoryItemCount => uint.TryParse(_historyItemCount.Value, out var value) ? value : 0;
public string ShowHistory => _showHistory.Value ?? string.Empty;
internal static string SettingsJsonPath()
{
@@ -91,11 +90,11 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
// Add the new history item
historyItems.Add(historyItem);
// Determine the maximum number of items to keep based on HistoryItemCount
if (HistoryItemCount > 0)
// Determine the maximum number of items to keep based on ShowHistory
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
// Keep only the most recent `maxHistoryItems` items
while (historyItems.Count > HistoryItemCount)
while (historyItems.Count > maxHistoryItems)
{
historyItems.RemoveAt(0); // Remove the oldest item
}
@@ -151,7 +150,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
_historyPath = HistoryStateJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount);
Settings.Add(_showHistory);
// Load settings from file upon initialization
LoadSettings();
@@ -189,11 +188,11 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
base.SaveSettings();
try
{
if (HistoryItemCount == 0)
if (ShowHistory == Resources.history_none)
{
ClearHistory();
}
else if (HistoryItemCount > 0)
else if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
// Trim the history file if there are more items than the new limit
if (File.Exists(_historyPath))
@@ -202,10 +201,10 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > HistoryItemCount)
if (historyItems.Count > maxHistoryItems)
{
// Trim the list to keep only the most recent `HistoryItemCount` items
historyItems = historyItems.Skip((int)(historyItems.Count - HistoryItemCount)).ToList();
// Trim the list to keep only the most recent `maxHistoryItems` items
historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList();
// Save the trimmed history back to the file
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);

View File

@@ -33,7 +33,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage
_allItems = [];
Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager;
_historyItems = _settingsManager.HistoryItemCount != 0 ? _settingsManager.LoadHistory() : null;
_historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null;
if (_historyItems is not null)
{
_allItems.AddRange(_historyItems);
@@ -57,7 +57,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage
if (_historyItems is not null)
{
filteredHistoryItems = _settingsManager.HistoryItemCount != 0 ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null;
filteredHistoryItems = _settingsManager.ShowHistory != Resources.history_none ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null;
}
var results = new List<ListItem>();

View File

@@ -168,15 +168,6 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Determines the number of history items to show from previous searches.
/// </summary>
public static string plugin_history_item_count {
get {
return ResourceManager.GetString("plugin_history_item_count", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to In the default browser.
/// </summary>
@@ -240,6 +231,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Determines the number of history items to show from previous searches.
/// </summary>
public static string plugin_show_history {
get {
return ResourceManager.GetString("plugin_show_history", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>

View File

@@ -172,7 +172,7 @@
<data name="plugin_search_failed" xml:space="preserve">
<value>Failed to open {0}.</value>
</data>
<data name="plugin_history_item_count" xml:space="preserve">
<data name="plugin_show_history" xml:space="preserve">
<value>Determines the number of history items to show from previous searches</value>
</data>
<data name="settings_page_name" xml:space="preserve">

View File

@@ -65,7 +65,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
if (_results is not null && _results.Count != 0)
{
var stopwatch = Stopwatch.StartNew();
var count = _results.Count;
var results = new ListItem[count];
var next = 0;
@@ -83,8 +82,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
}
}
stopwatch.Stop();
Logger.LogDebug($"Building ListItems took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(GetItems));
IsLoading = false;
return results;
}
@@ -247,22 +244,15 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
// foreach (var catalog in connections)
{
Stopwatch findPackages_stopwatch = new();
findPackages_stopwatch.Start();
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
ct.ThrowIfCancellationRequested();
Logger.LogDebug($"Preface for \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
// BODGY, re: microsoft/winget-cli#5151
// FindPackagesAsync isn't actually async.
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
var searchResults = await internalSearchTask;
findPackages_stopwatch.Stop();
Logger.LogDebug($"FindPackages for \"{searchDebugText}\" took {findPackages_stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
// TODO more error handling like this:
if (searchResults.Status != FindPackagesResultStatus.Ok)
{
@@ -271,8 +261,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
return [];
}
ct.ThrowIfCancellationRequested();
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
// FYI Using .ToArray or any other kind of enumerable loop

View File

@@ -43,18 +43,13 @@ public partial class ListHelpers
}
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
{
return FilterListWithScores<T>(items, query, scoreFunction)
.Select(score => score.Item);
}
public static IEnumerable<Scored<T>> FilterListWithScores<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
{
var scores = items
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })
.Where(score => score.Score > 0)
.OrderByDescending(score => score.Score);
return scores;
return scores
.Select(score => score.Item);
}
/// <summary>

View File

@@ -372,17 +372,20 @@ namespace UITests_FancyZones
// launch FancyZones settings page
private void LaunchFancyZones()
{
this.Find<NavigationViewItem>(By.AccessibilityId("WindowingAndLayoutsNavItem")).Click();
if (this.FindAll<NavigationViewItem>("FancyZones").Count == 0)
{
this.Find<NavigationViewItem>("Windowing & Layouts").Click();
}
this.Find<NavigationViewItem>(By.AccessibilityId("FancyZonesNavItem")).Click();
this.Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true);
this.Session.SetMainWindowSize(WindowSize.Large);
Find<Element>(By.AccessibilityId("HeaderPresenter")).Click();
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
ZoneBehaviourSettings(TestContext.TestName);
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000);
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 500, 10000);
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.
@@ -400,7 +403,7 @@ namespace UITests_FancyZones
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
this.Session.Attach(PowerToysModule.PowerToysSettings);
SetupCustomLayouts();
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 5000, 5000);
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 5000, 5000);
this.Session.Attach(PowerToysModule.FancyZone);
this.Find<Microsoft.PowerToys.UITest.Button>("Maximize").Click();

View File

@@ -43,32 +43,11 @@ private:
//contains the non localized key of the powertoy
std::wstring app_key;
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
Logger::info(L"ImageResizer context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered");
#endif
}
}
public:
// Constructor
ImageResizerModule()
{
m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
app_name = GET_RESOURCE_STRING(IDS_IMAGERESIZER);
app_key = ImageResizerConstants::ModuleKey;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::imageResizerLoggerName);
@@ -133,7 +112,10 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
UpdateRegistration(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
#endif
Trace::EnableImageResizer(m_enabled);
}
@@ -141,8 +123,11 @@ public:
virtual void disable()
{
m_enabled = false;
UpdateRegistration(m_enabled);
Trace::EnableImageResizer(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered (Win10)");
#endif
}
// Returns if the powertoys is enabled

View File

@@ -168,25 +168,6 @@ private:
//contains the non localized key of the powertoy
std::wstring app_key;
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
Logger::info(L"PowerRename context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered");
#endif
}
}
public:
// Return the localized display name of the powertoy
virtual PCWSTR get_name() override
@@ -221,7 +202,9 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
UpdateRegistration(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
#endif
}
// Disable the powertoy
@@ -229,7 +212,10 @@ public:
{
m_enabled = false;
Logger::info(L"PowerRename disabled");
UpdateRegistration(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered (Win10)");
#endif
}
// Returns if the powertoy is enabled
@@ -329,7 +315,6 @@ public:
void init_settings()
{
m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
Trace::EnablePowerRename(m_enabled);
}

View File

@@ -15,10 +15,10 @@ namespace RegistryPreview
/// </summary>
private void AppWindow_Closing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args)
{
jsonWindowPlacement.SetNamedValue("appWindow.Position.X", JsonValue.CreateNumberValue(AppWindow.Position.X));
jsonWindowPlacement.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(AppWindow.Position.Y));
jsonWindowPlacement.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(AppWindow.Size.Width));
jsonWindowPlacement.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(AppWindow.Size.Height));
jsonWindowPlacement.SetNamedValue("appWindow.Position.X", JsonValue.CreateNumberValue(appWindow.Position.X));
jsonWindowPlacement.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(appWindow.Position.Y));
jsonWindowPlacement.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(appWindow.Size.Width));
jsonWindowPlacement.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(appWindow.Size.Height));
}
/// <summary>

View File

@@ -15,19 +15,38 @@
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid x:Name="MainGrid" Loaded="Grid_Loaded">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="16,0,0,0"
Source="/Assets/RegistryPreview/RegistryPreview.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<Grid
x:Name="titleBar"
Grid.Row="0"
Height="32"
Margin="16,0"
ColumnSpacing="16"
IsHitTestVisible="True">
<Grid.ColumnDefinitions>
<!--<ColumnDefinition x:Name="LeftPaddingColumn" Width="0"/>-->
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<!--<ColumnDefinition x:Name="RightPaddingColumn" Width="0"/>-->
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/RegistryPreview/RegistryPreview.ico" />
<TextBlock
x:Name="titleBarText"
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding ApplicationTitle}" />
</Grid>
</Grid>
</winuiex:WindowEx>
</winuiex:WindowEx>

View File

@@ -6,7 +6,6 @@ using System;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
@@ -24,6 +23,7 @@ namespace RegistryPreview
private const string APPNAME = "RegistryPreview";
// private members
private Microsoft.UI.Windowing.AppWindow appWindow;
private JsonObject jsonWindowPlacement;
private string settingsFolder = string.Empty;
private string windowPlacementFile = "app-placement.json";
@@ -38,15 +38,20 @@ namespace RegistryPreview
settingsFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerToys\" + APPNAME;
OpenWindowPlacementFile(settingsFolder, windowPlacementFile);
// Update the Win32 looking window with the correct icon (and grab the appWindow handle for later)
IntPtr windowHandle = this.GetWindowHandle();
WindowId windowId = Win32Interop.GetWindowIdFromWindow(windowHandle);
appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
appWindow.SetIcon("Assets\\RegistryPreview\\RegistryPreview.ico");
// TODO(stefan)
AppWindow.Closing += AppWindow_Closing;
appWindow.Closing += AppWindow_Closing;
Activated += MainWindow_Activated;
// Extend the canvas to include the title bar so the app can support theming
ExtendsContentIntoTitleBar = true;
IntPtr windowHandle = this.GetWindowHandle();
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(windowHandle);
SetTitleBar(titleBar);
AppWindow.SetIcon("Assets\\RegistryPreview\\RegistryPreview.ico");
// if have settings, update the location of the window
if (jsonWindowPlacement != null)
@@ -61,7 +66,7 @@ namespace RegistryPreview
// check to make sure the size values are reasonable before attempting to restore the last saved size
if (size.Width >= 320 && size.Height >= 240)
{
AppWindow.Resize(size);
appWindow.Resize(size);
}
}
@@ -75,7 +80,7 @@ namespace RegistryPreview
// check to make sure the move values are reasonable before attempting to restore the last saved location
if (point.X >= 0 && point.Y >= 0)
{
AppWindow.Move(point);
appWindow.Move(point);
}
}
}
@@ -87,6 +92,20 @@ namespace RegistryPreview
PowerToysTelemetry.Log.WriteEvent(new RegistryPreviewEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
titleBarText.Foreground =
(SolidColorBrush)Application.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
titleBarText.Foreground =
(SolidColorBrush)Application.Current.Resources["WindowCaptionForeground"];
}
}
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
MainGrid.Children.Add(MainPage);
@@ -99,23 +118,23 @@ namespace RegistryPreview
if (string.IsNullOrEmpty(filename))
{
titleBar.Title = APPNAME;
AppWindow.Title = APPNAME;
titleBarText.Text = APPNAME;
appWindow.Title = APPNAME;
}
else
{
string[] file = filename.Split('\\');
if (file.Length > 0)
{
titleBar.Title = file[file.Length - 1] + " - " + APPNAME;
titleBarText.Text = file[file.Length - 1] + " - " + APPNAME;
}
else
{
titleBar.Title = filename + " - " + APPNAME;
titleBarText.Text = filename + " - " + APPNAME;
}
// Continue to update the window's title, after updating the custom title bar
AppWindow.Title = titleBar.Title;
appWindow.Title = titleBarText.Text;
}
}
}

View File

@@ -19,13 +19,6 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
"ShellPage.xaml",
};
// Hardcoded panel-to-page mapping (temporary until generic panel host mapping is needed)
// Key: panel file base name (without .xaml), Value: owning page base name
private static readonly Dictionary<string, string> PanelPageMapping = new(StringComparer.OrdinalIgnoreCase)
{
{ "MouseJumpPanel", "MouseUtilsPage" },
};
private static JsonSerializerOptions serializeOption = new()
{
WriteIndented = true,
@@ -40,117 +33,32 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
Environment.Exit(1);
}
string xamlRootDirectory = args[0];
string xamlDirectory = args[0];
string outputFile = args[1];
if (!Directory.Exists(xamlRootDirectory))
if (!Directory.Exists(xamlDirectory))
{
Debug.WriteLine($"Error: Directory '{xamlRootDirectory}' does not exist.");
Debug.WriteLine($"Error: Directory '{xamlDirectory}' does not exist.");
Environment.Exit(1);
}
try
{
var searchableElements = new List<SettingEntry>();
var processedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var xamlFiles = Directory.GetFiles(xamlDirectory, "*.xaml", SearchOption.AllDirectories);
void ScanDirectory(string root)
foreach (var xamlFile in xamlFiles)
{
if (!Directory.Exists(root))
var fileName = Path.GetFileName(xamlFile);
if (ExcludedXamlFiles.Contains(fileName))
{
return;
// Skip ShellPage.xaml as it contains many elements not relevant for search
continue;
}
Debug.WriteLine($"[XamlIndexBuilder] Scanning root: {root}");
var xamlFilesLocal = Directory.GetFiles(root, "*.xaml", SearchOption.AllDirectories);
foreach (var xamlFile in xamlFilesLocal)
{
var fullPath = Path.GetFullPath(xamlFile);
if (processedFiles.Contains(fullPath))
{
continue; // already handled (can happen if overlapping directories)
}
var fileName = Path.GetFileName(xamlFile);
if (ExcludedXamlFiles.Contains(fileName))
{
continue; // explicitly excluded
}
Debug.WriteLine($"Processing: {fileName}");
var elements = ExtractSearchableElements(xamlFile);
// Apply hardcoded panel mapping override
var baseName = Path.GetFileNameWithoutExtension(xamlFile);
if (PanelPageMapping.TryGetValue(baseName, out var hostPage))
{
for (int i = 0; i < elements.Count; i++)
{
var entry = elements[i];
entry.PageTypeName = hostPage;
elements[i] = entry;
}
}
searchableElements.AddRange(elements);
processedFiles.Add(fullPath);
}
}
// Scan well-known subdirectories under the provided root
var subDirs = new[] { "Views", "Panels" };
foreach (var sub in subDirs)
{
ScanDirectory(Path.Combine(xamlRootDirectory, sub));
}
// Fallback: also scan root directly (in case some XAML lives at root level)
ScanDirectory(xamlRootDirectory);
// -----------------------------------------------------------------------------
// Explicit include section: add specific XAML files that we always want indexed
// even if future logic excludes them or they live outside typical scan patterns.
// Add future files to the ExplicitExtraXamlFiles array below.
// -----------------------------------------------------------------------------
string[] explicitExtraXamlFiles = new[]
{
"MouseJumpPanel.xaml", // Mouse Jump settings panel
};
foreach (var extraFileName in explicitExtraXamlFiles)
{
try
{
var matches = Directory.GetFiles(xamlRootDirectory, extraFileName, SearchOption.AllDirectories);
foreach (var match in matches)
{
var full = Path.GetFullPath(match);
if (processedFiles.Contains(full))
{
continue; // already processed in general scan
}
Debug.WriteLine($"Processing (explicit include): {extraFileName}");
var elements = ExtractSearchableElements(full);
var baseName = Path.GetFileNameWithoutExtension(full);
if (PanelPageMapping.TryGetValue(baseName, out var hostPage))
{
for (int i = 0; i < elements.Count; i++)
{
var entry = elements[i];
entry.PageTypeName = hostPage;
elements[i] = entry;
}
}
searchableElements.AddRange(elements);
processedFiles.Add(full);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Explicit include failed for {extraFileName}: {ex.Message}");
}
Debug.WriteLine($"Processing: {fileName}");
var elements = ExtractSearchableElements(xamlFile);
searchableElements.AddRange(elements);
}
searchableElements = searchableElements.OrderBy(e => e.PageTypeName).ThenBy(e => e.ElementName).ToList();
@@ -189,15 +97,15 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
.Where(e => e.Name.LocalName == "SettingsPageControl")
.Where(e => e.Attribute(x + "Uid") != null);
// Extract SettingsCard elements (support both Name and x:Name)
// Extract SettingsCard elements
var settingsElements = doc.Descendants()
.Where(e => e.Name.LocalName == "SettingsCard")
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Name") != null || e.Attribute(x + "Uid") != null);
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
// Extract SettingsExpander elements (support both Name and x:Name)
// Extract SettingsExpander elements
var settingsExpanderElements = doc.Descendants()
.Where(e => e.Name.LocalName == "SettingsExpander")
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Name") != null || e.Attribute(x + "Uid") != null);
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
// Process SettingsPageControl elements
foreach (var element in settingsPageElements)
@@ -277,36 +185,16 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
public static string GetElementName(XElement element, XNamespace x)
{
// Get Name attribute (we call it ElementName in our indexing system)
var name = element.Attribute("Name")?.Value;
if (string.IsNullOrEmpty(name))
{
name = element.Attribute(x + "Name")?.Value;
}
return name;
}
public static string GetElementUid(XElement element, XNamespace x)
{
// Try x:Uid on the element itself
// Try x:Uid
var uid = element.Attribute(x + "Uid")?.Value;
if (!string.IsNullOrWhiteSpace(uid))
{
return uid;
}
// Fallback: check the first direct child element's x:Uid
var firstChild = element.Elements().FirstOrDefault();
if (firstChild != null)
{
var childUid = firstChild.Attribute(x + "Uid")?.Value;
if (!string.IsNullOrWhiteSpace(childUid))
{
return childUid;
}
}
return null;
return uid;
}
public static string GetParentElementName(XElement element, XNamespace x)
@@ -323,11 +211,6 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
if (expanderParent?.Name.LocalName == "SettingsExpander")
{
var expanderName = expanderParent.Attribute("Name")?.Value;
if (string.IsNullOrEmpty(expanderName))
{
expanderName = expanderParent.Attribute(x + "Name")?.Value;
}
if (!string.IsNullOrEmpty(expanderName))
{
return expanderName;
@@ -338,11 +221,6 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
{
// Direct child of SettingsExpander
var expanderName = current.Attribute("Name")?.Value;
if (string.IsNullOrEmpty(expanderName))
{
expanderName = current.Attribute(x + "Name")?.Value;
}
if (!string.IsNullOrEmpty(expanderName))
{
return expanderName;

View File

@@ -29,15 +29,16 @@
<!-- Remove UI library reference to avoid pulling WindowsDesktop runtime (WindowsBase) -->
<PropertyGroup>
<!-- Fallback to dotnet if not provided by the environment -->
<DotNetExe Condition="'$(DotNetExe)' == ''">dotnet</DotNetExe>
<XamlRootDir Condition="'$(XamlRootDir)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML</XamlRootDir>
<XamlRootDir Condition="'$(XamlViewsDir)' != ''">$([System.IO.Path]::GetDirectoryName('$(XamlViewsDir)'))</XamlRootDir>
<XamlViewsDir Condition="'$(XamlViewsDir)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML\Views</XamlViewsDir>
<GeneratedJsonFile Condition="'$(GeneratedJsonFile)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\Assets\Settings\search.index.json</GeneratedJsonFile>
</PropertyGroup>
<Target Name="GenerateSearchIndexSelf" AfterTargets="Build">
<RemoveDir Directories="$(MSBuildProjectDirectory)\obj\ARM64;$(MSBuildProjectDirectory)\obj\x64;$(MSBuildProjectDirectory)\bin" />
<MakeDir Directories="$([System.IO.Path]::GetDirectoryName('$(GeneratedJsonFile)'))" />
<Message Importance="high" Text="[XamlIndexBuilder] Generating search index. Root='$(XamlRootDir)'; Out='$(GeneratedJsonFile)'; Tool='$(TargetPath)'; DotNet='$(DotNetExe)'." />
<Exec Command="&quot;$(DotNetExe)&quot; &quot;$(TargetPath)&quot; &quot;$(XamlRootDir)&quot; &quot;$(GeneratedJsonFile)&quot;" />
<Message Importance="high" Text="[XamlIndexBuilder] Generating search index. Views='$(XamlViewsDir)'; Out='$(GeneratedJsonFile)'; Tool='$(TargetPath)'; DotNet='$(DotNetExe)'." />
<!-- Execute via dotnet so host architecture doesn't need to match -->
<Exec Command="&quot;$(DotNetExe)&quot; &quot;$(TargetPath)&quot; &quot;$(XamlViewsDir)&quot; &quot;$(GeneratedJsonFile)&quot;" />
</Target>
</Project>

View File

@@ -1,244 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Numerics;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.PowerToys.Settings.UI.Helpers;
public abstract partial class NavigablePage : Page
{
private const int ExpandWaitDuration = 500;
private const int AnimationDuration = 1850;
private NavigationParams _pendingNavigationParams;
public NavigablePage()
{
Loaded += OnPageLoaded;
}
protected override void OnNavigatedTo(Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Handle both old string parameter and new NavigationParams
if (e.Parameter is NavigationParams navParams)
{
_pendingNavigationParams = navParams;
}
else if (e.Parameter is string elementKey)
{
_pendingNavigationParams = new NavigationParams(elementKey);
}
}
private async void OnPageLoaded(object sender, RoutedEventArgs e)
{
if (_pendingNavigationParams != null && !string.IsNullOrEmpty(_pendingNavigationParams.ElementName))
{
// First, expand parent if specified
if (!string.IsNullOrEmpty(_pendingNavigationParams.ParentElementName))
{
var parentElement = FindElementByName(_pendingNavigationParams.ParentElementName);
if (parentElement is SettingsExpander expander)
{
expander.IsExpanded = true;
// Give time for the expander to animate
await Task.Delay(ExpandWaitDuration);
}
}
// Then find and navigate to the target element
var target = FindElementByName(_pendingNavigationParams.ElementName);
target?.StartBringIntoView(new BringIntoViewOptions
{
VerticalOffset = -20,
AnimationDesired = true,
});
await OnTargetElementNavigatedAsync(target, _pendingNavigationParams.ElementName);
_pendingNavigationParams = null;
}
}
protected virtual async Task OnTargetElementNavigatedAsync(FrameworkElement target, string elementKey)
{
if (target == null)
{
return;
}
// Attempt to set keyboard focus so that screen readers announce the element and keyboard users land directly on it.
TrySetFocus(target);
// Get the visual and compositor
var visual = ElementCompositionPreview.GetElementVisual(target);
var compositor = visual.Compositor;
// Create a subtle glow effect using drop shadow
var dropShadow = compositor.CreateDropShadow();
dropShadow.Color = (Color)Application.Current.Resources["SystemAccentColorLight2"];
dropShadow.BlurRadius = 16f;
dropShadow.Opacity = 0f;
dropShadow.Offset = new Vector3(0, 0, 0);
var spriteVisual = compositor.CreateSpriteVisual();
spriteVisual.Size = new Vector2((float)target.ActualWidth + 8, (float)target.ActualHeight + 8);
spriteVisual.Shadow = dropShadow;
spriteVisual.Offset = new Vector3(-4, -4, 0);
// Insert the shadow visual behind the target element
ElementCompositionPreview.SetElementChildVisual(target, spriteVisual);
// Create a simple fade in/out animation
var fadeAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeAnimation.InsertKeyFrame(0f, 0f);
fadeAnimation.InsertKeyFrame(0.5f, 0.3f);
fadeAnimation.InsertKeyFrame(1f, 0f);
fadeAnimation.Duration = TimeSpan.FromMilliseconds(AnimationDuration);
dropShadow.StartAnimation("Opacity", fadeAnimation);
await Task.Delay(AnimationDuration);
// Clean up the shadow visual
ElementCompositionPreview.SetElementChildVisual(target, null);
}
private static void TrySetFocus(FrameworkElement target)
{
try
{
// Prefer Control.Focus when available.
if (target is Control ctrl)
{
// Ensure it can receive focus.
if (!ctrl.IsTabStop)
{
ctrl.IsTabStop = true;
}
ctrl.Focus(FocusState.Programmatic);
}
// Target is not a Control. Find first focusable descendant Control.
var focusCandidate = FindFirstFocusableDescendant(target);
if (focusCandidate != null)
{
if (!focusCandidate.IsTabStop)
{
focusCandidate.IsTabStop = true;
}
focusCandidate.Focus(FocusState.Programmatic);
return;
}
// Fallback: attempt to focus parent control if no descendant found.
if (target.Parent is Control parent)
{
if (!parent.IsTabStop)
{
parent.IsTabStop = true;
}
parent.Focus(FocusState.Programmatic);
}
}
catch
{
// Swallow focus exceptions; not critical. Could log if logging enabled.
// Leave the default focus as it is.
}
}
private static Control FindFirstFocusableDescendant(FrameworkElement root)
{
if (root == null)
{
return null;
}
var queue = new System.Collections.Generic.Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current is Control c && c.IsEnabled && c.Visibility == Visibility.Visible)
{
return c;
}
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
queue.Enqueue(VisualTreeHelper.GetChild(current, i));
}
}
return null;
}
protected FrameworkElement FindElementByName(string name)
{
var element = this.FindName(name) as FrameworkElement;
if (element != null)
{
return element;
}
if (this.Content is DependencyObject root)
{
var found = FindInDescendants(root, name);
if (found != null)
{
return found;
}
}
return null;
}
private static FrameworkElement FindInDescendants(DependencyObject root, string name)
{
if (root == null || string.IsNullOrEmpty(name))
{
return null;
}
var queue = new System.Collections.Generic.Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current is FrameworkElement fe)
{
var local = fe.FindName(name) as FrameworkElement;
if (local != null)
{
return local;
}
}
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(current, i);
queue.Enqueue(child);
}
}
return null;
}
}

View File

@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Numerics;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
namespace Microsoft.PowerToys.Settings.UI.Helpers;
public abstract partial class NavigatablePage : Page
{
private const int ExpandWaitDuration = 500;
private const int AnimationDuration = 1000;
private NavigationParams _pendingNavigationParams;
public NavigatablePage()
{
Loaded += OnPageLoaded;
}
protected override void OnNavigatedTo(Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Handle both old string parameter and new NavigationParams
if (e.Parameter is NavigationParams navParams)
{
_pendingNavigationParams = navParams;
}
else if (e.Parameter is string elementKey)
{
_pendingNavigationParams = new NavigationParams(elementKey);
}
}
private async void OnPageLoaded(object sender, RoutedEventArgs e)
{
if (_pendingNavigationParams != null && !string.IsNullOrEmpty(_pendingNavigationParams.ElementName))
{
// First, expand parent if specified
if (!string.IsNullOrEmpty(_pendingNavigationParams.ParentElementName))
{
var parentElement = FindElementByName(_pendingNavigationParams.ParentElementName);
if (parentElement is SettingsExpander expander)
{
expander.IsExpanded = true;
// Give time for the expander to animate
await Task.Delay(ExpandWaitDuration);
}
}
// Then find and navigate to the target element
var target = FindElementByName(_pendingNavigationParams.ElementName);
target?.StartBringIntoView(new BringIntoViewOptions
{
VerticalOffset = -20,
AnimationDesired = true,
});
await OnTargetElementNavigatedAsync(target, _pendingNavigationParams.ElementName);
_pendingNavigationParams = null;
}
}
protected virtual async Task OnTargetElementNavigatedAsync(FrameworkElement target, string elementKey)
{
if (target == null)
{
return;
}
// Get the visual and compositor
var visual = ElementCompositionPreview.GetElementVisual(target);
var compositor = visual.Compositor;
// Create a subtle glow effect using drop shadow
var dropShadow = compositor.CreateDropShadow();
dropShadow.Color = Microsoft.UI.Colors.Gray;
dropShadow.BlurRadius = 8f;
dropShadow.Opacity = 0f;
dropShadow.Offset = new Vector3(0, 0, 0);
var spriteVisual = compositor.CreateSpriteVisual();
spriteVisual.Size = new Vector2((float)target.ActualWidth + 16, (float)target.ActualHeight + 16);
spriteVisual.Shadow = dropShadow;
spriteVisual.Offset = new Vector3(-8, -8, 0);
// Insert the shadow visual behind the target element
ElementCompositionPreview.SetElementChildVisual(target, spriteVisual);
// Create a simple fade in/out animation
var fadeAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeAnimation.InsertKeyFrame(0f, 0f);
fadeAnimation.InsertKeyFrame(0.5f, 0.3f);
fadeAnimation.InsertKeyFrame(1f, 0f);
fadeAnimation.Duration = TimeSpan.FromMilliseconds(AnimationDuration);
dropShadow.StartAnimation("Opacity", fadeAnimation);
if (target is Control ctrl)
{
// TODO: ability to adjust brush color and animation from settings.
var originalBackground = ctrl.Background;
var highlightBrush = new SolidColorBrush();
var grayColor = Microsoft.UI.Colors.Gray;
grayColor.A = 50; // Very subtle transparency
highlightBrush.Color = grayColor;
// Apply the highlight
ctrl.Background = highlightBrush;
// Wait for animation to complete
await Task.Delay(AnimationDuration);
// Restore original background
ctrl.Background = originalBackground;
}
else
{
// For non-control elements, just wait for the glow animation
await Task.Delay(AnimationDuration);
}
// Clean up the shadow visual
ElementCompositionPreview.SetElementChildVisual(target, null);
}
protected FrameworkElement FindElementByName(string name)
{
var element = this.FindName(name) as FrameworkElement;
return element;
}
}

View File

@@ -25,7 +25,6 @@
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
<None Remove="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml" />
<None Remove="SettingsXAML\Controls\TitleBar\TitleBar.xaml" />
</ItemGroup>
<ItemGroup>
<Page Remove="SettingsXAML\App.xaml" />
@@ -64,6 +63,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.TitleBar" />
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />
@@ -156,8 +156,7 @@
</None>
<None Update="Assets\Settings\Scripts\DisableModule.ps1">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> <Page Update="SettingsXAML\Controls\TitleBar\TitleBar.xaml">
</Page>
</None>
<Page Update="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
@@ -181,8 +180,14 @@
</Page>
</ItemGroup>
<!-- Removed nested publish/exec and copy targets. -->
<!-- Build XamlIndexBuilder before compiling Settings to ensure the search index exists without taking a project reference. -->
<Target Name="BuildXamlIndexBeforeSettings" BeforeTargets="CoreCompile">
<Message Importance="high" Text="[Settings] Building XamlIndexBuilder prior to compile. Views='$(MSBuildProjectDirectory)\SettingsXAML\Views' Out='$(GeneratedJsonFile)'" />
<MSBuild Projects="..\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj" Targets="Build" Properties="Configuration=$(Configuration);Platform=Any CPU;TargetFramework=net9.0;XamlViewsDir=$(MSBuildProjectDirectory)\SettingsXAML\Views;GeneratedJsonFile=$(GeneratedJsonFile)" />
<MSBuild
Projects="..\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj"
Targets="Build"
Properties="Configuration=$(Configuration);Platform=Any CPU;TargetFramework=net9.0;XamlViewsDir=$(MSBuildProjectDirectory)\SettingsXAML\Views;GeneratedJsonFile=$(GeneratedJsonFile)" />
</Target>
</Project>

View File

@@ -170,7 +170,7 @@ namespace Microsoft.PowerToys.Settings.UI.Services
if (string.IsNullOrEmpty(header))
{
header = GetString(resourceLoader, $"{elementUid}/Content");
Debug.WriteLine($"[SearchIndexService] WARNING: No header localization found for ElementUid: '{elementUid}'");
}
return (header, description);

View File

@@ -11,7 +11,6 @@
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml" />
<ResourceDictionary Source="/SettingsXAML/Controls/TitleBar/TitleBar.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/TextBlock.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/Button.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/InfoBadge.xaml" />

View File

@@ -548,14 +548,18 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
}
}
var moduleNames = conflictingModules.ToArray();
if (string.Equals(moduleNames[0], "System", StringComparison.OrdinalIgnoreCase))
if (conflictingModules.Count > 0)
{
c.ConflictMessage = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText");
var moduleNames = conflictingModules.ToArray();
var conflictMessage = moduleNames.Length == 1
? $"Conflict detected with {moduleNames[0]}"
: $"Conflicts detected with: {string.Join(", ", moduleNames)}";
c.ConflictMessage = conflictMessage;
}
else
{
c.ConflictMessage = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
c.ConflictMessage = "Conflict detected with unknown module";
}
c.HasConflict = true;

View File

@@ -64,7 +64,7 @@
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
Severity="Warning" />
<InfoBar
x:Uid="WarningShortcutConflict"
Title="Hotkey Conflict"
IsClosable="False"
IsOpen="{Binding ElementName=ShortcutContentControl, Path=HasConflict, Mode=OneWay}"
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=HasConflict, Mode=OneWay}"

View File

@@ -1,229 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Controls;
public partial class TitleBar : Control
{
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Icon"/> property.
/// </summary>
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(IconElement), typeof(TitleBar), new PropertyMetadata(null, IconChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Title"/> property.
/// </summary>
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(TitleBar), new PropertyMetadata(default(string)));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Subtitle"/> property.
/// </summary>
public static readonly DependencyProperty SubtitleProperty = DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(TitleBar), new PropertyMetadata(default(string)));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Content"/> property.
/// </summary>
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(TitleBar), new PropertyMetadata(null, ContentChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Footer"/> property.
/// </summary>
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(nameof(Footer), typeof(object), typeof(TitleBar), new PropertyMetadata(null, FooterChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="IsBackButtonVisible"/> property.
/// </summary>
public static readonly DependencyProperty IsBackButtonVisibleProperty = DependencyProperty.Register(nameof(IsBackButtonVisible), typeof(bool), typeof(TitleBar), new PropertyMetadata(false, IsBackButtonVisibleChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="IsPaneButtonVisible"/> property.
/// </summary>
public static readonly DependencyProperty IsPaneButtonVisibleProperty = DependencyProperty.Register(nameof(IsPaneButtonVisible), typeof(bool), typeof(TitleBar), new PropertyMetadata(false, IsPaneButtonVisibleChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="DisplayMode"/> property.
/// </summary>
public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(DisplayMode), typeof(TitleBar), new PropertyMetadata(DisplayMode.Standard, DisplayModeChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="CompactStateBreakpoint
/// "/> property.
/// </summary>
public static readonly DependencyProperty CompactStateBreakpointProperty = DependencyProperty.Register(nameof(CompactStateBreakpoint), typeof(int), typeof(TitleBar), new PropertyMetadata(850));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="AutoConfigureCustomTitleBar"/> property.
/// </summary>
public static readonly DependencyProperty AutoConfigureCustomTitleBarProperty = DependencyProperty.Register(nameof(AutoConfigureCustomTitleBar), typeof(bool), typeof(TitleBar), new PropertyMetadata(true, AutoConfigureCustomTitleBarChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Window"/> property.
/// </summary>
public static readonly DependencyProperty WindowProperty = DependencyProperty.Register(nameof(Window), typeof(Window), typeof(TitleBar), new PropertyMetadata(null));
/// <summary>
/// The event that gets fired when the back button is clicked
/// </summary>
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public event EventHandler<RoutedEventArgs>? BackButtonClick;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
/// <summary>
/// The event that gets fired when the pane toggle button is clicked
/// </summary>
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public event EventHandler<RoutedEventArgs>? PaneButtonClick;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
/// <summary>
/// Gets or sets the Icon
/// </summary>
public IconElement Icon
{
get => (IconElement)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets the Title
/// </summary>
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
/// <summary>
/// Gets or sets the Subtitle
/// </summary>
public string Subtitle
{
get => (string)GetValue(SubtitleProperty);
set => SetValue(SubtitleProperty, value);
}
/// <summary>
/// Gets or sets the content shown at the center of the TitleBar. When setting this, using DisplayMode=Tall is recommended.
/// </summary>
public object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
/// <summary>
/// Gets or sets the content shown at the right of the TitleBar, next to the caption buttons. When setting this, using DisplayMode=Tall is recommended.
/// </summary>
public object Footer
{
get => (object)GetValue(FooterProperty);
set => SetValue(FooterProperty, value);
}
/// <summary>
/// Gets or sets DisplayMode. Compact is default (32px), Tall is recommended when setting the Content or Footer.
/// </summary>
public DisplayMode DisplayMode
{
get => (DisplayMode)GetValue(DisplayModeProperty);
set => SetValue(DisplayModeProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets the visibility of the back button.
/// </summary>
public bool IsBackButtonVisible
{
get => (bool)GetValue(IsBackButtonVisibleProperty);
set => SetValue(IsBackButtonVisibleProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets the visibility of the pane toggle button.
/// </summary>
public bool IsPaneButtonVisible
{
get => (bool)GetValue(IsPaneButtonVisibleProperty);
set => SetValue(IsPaneButtonVisibleProperty, value);
}
/// <summary>
/// Gets or sets the breakpoint of when the compact state is triggered.
/// </summary>
public int CompactStateBreakpoint
{
get => (int)GetValue(CompactStateBreakpointProperty);
set => SetValue(CompactStateBreakpointProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets if the TitleBar should auto configure ExtendContentIntoTitleBar and CaptionButton background colors.
/// </summary>
public bool AutoConfigureCustomTitleBar
{
get => (bool)GetValue(AutoConfigureCustomTitleBarProperty);
set => SetValue(AutoConfigureCustomTitleBarProperty, value);
}
/// <summary>
/// Gets or sets the window the TitleBar should configure.
/// </summary>
public Window Window
{
get => (Window)GetValue(WindowProperty);
set => SetValue(WindowProperty, value);
}
private static void IsBackButtonVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void IsPaneButtonVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void DisplayModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void ContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void FooterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void IconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void AutoConfigureCustomTitleBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (((TitleBar)d).AutoConfigureCustomTitleBar)
{
((TitleBar)d).Configure();
}
else
{
((TitleBar)d).Reset();
}
}
}
public enum DisplayMode
{
Standard,
Tall,
}

View File

@@ -1,195 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.UI;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.Graphics;
namespace Microsoft.PowerToys.Settings.UI.Controls;
[TemplatePart(Name = nameof(PART_FooterPresenter), Type = typeof(ContentPresenter))]
[TemplatePart(Name = nameof(PART_ContentPresenter), Type = typeof(ContentPresenter))]
public partial class TitleBar : Control
{
#pragma warning disable SA1306 // Field names should begin with lower-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
#pragma warning disable SA1400 // Access modifier should be declared
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
ContentPresenter? PART_ContentPresenter;
ContentPresenter? PART_FooterPresenter;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore SA1400 // Access modifier should be declared
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore
private void SetWASDKTitleBar()
{
if (this.Window == null)
{
return;
}
if (AutoConfigureCustomTitleBar)
{
Window.AppWindow.TitleBar.ExtendsContentIntoTitleBar = true;
this.Window.SizeChanged -= Window_SizeChanged;
this.Window.SizeChanged += Window_SizeChanged;
this.Window.Activated -= Window_Activated;
this.Window.Activated += Window_Activated;
if (Window.Content is FrameworkElement rootElement)
{
UpdateCaptionButtons(rootElement);
rootElement.ActualThemeChanged += (s, e) =>
{
UpdateCaptionButtons(rootElement);
};
}
PART_ContentPresenter = GetTemplateChild(nameof(PART_ContentPresenter)) as ContentPresenter;
PART_FooterPresenter = GetTemplateChild(nameof(PART_FooterPresenter)) as ContentPresenter;
// Get caption button occlusion information.
int captionButtonOcclusionWidthRight = Window.AppWindow.TitleBar.RightInset;
int captionButtonOcclusionWidthLeft = Window.AppWindow.TitleBar.LeftInset;
PART_LeftPaddingColumn!.Width = new GridLength(captionButtonOcclusionWidthLeft);
PART_RightPaddingColumn!.Width = new GridLength(captionButtonOcclusionWidthRight);
if (DisplayMode == DisplayMode.Tall)
{
// Choose a tall title bar to provide more room for interactive elements
// like search box or person picture controls.
Window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
}
else
{
Window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Standard;
}
// Recalculate the drag region for the custom title bar
// if you explicitly defined new draggable areas.
SetDragRegionForCustomTitleBar();
_isAutoConfigCompleted = true;
}
}
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
UpdateVisualStateAndDragRegion(args.Size);
}
private void UpdateCaptionButtons(FrameworkElement rootElement)
{
Window.AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
Window.AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
if (rootElement.ActualTheme == ElementTheme.Dark)
{
Window.AppWindow.TitleBar.ButtonForegroundColor = Colors.White;
Window.AppWindow.TitleBar.ButtonInactiveForegroundColor = Colors.DarkGray;
}
else
{
Window.AppWindow.TitleBar.ButtonForegroundColor = Colors.Black;
Window.AppWindow.TitleBar.ButtonInactiveForegroundColor = Colors.DarkGray;
}
}
private void ResetWASDKTitleBar()
{
if (this.Window == null)
{
return;
}
// Only reset if we were the ones who configured
if (_isAutoConfigCompleted)
{
Window.AppWindow.TitleBar.ExtendsContentIntoTitleBar = false;
this.Window.SizeChanged -= Window_SizeChanged;
this.Window.Activated -= Window_Activated;
SizeChanged -= this.TitleBar_SizeChanged;
Window.AppWindow.TitleBar.ResetToDefault();
}
}
private void Window_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
VisualStateManager.GoToState(this, WindowDeactivatedState, true);
}
else
{
VisualStateManager.GoToState(this, WindowActivatedState, true);
}
}
public void SetDragRegionForCustomTitleBar()
{
if (AutoConfigureCustomTitleBar && Window is not null)
{
ClearDragRegions(NonClientRegionKind.Passthrough);
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var items = new FrameworkElement?[] { PART_ContentPresenter, PART_FooterPresenter, PART_ButtonHolder };
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var validItems = items.Where(x => x is not null).Select(x => x!).ToArray(); // Prune null items
SetDragRegion(NonClientRegionKind.Passthrough, validItems);
}
}
public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
return element.XamlRoot.RasterizationScale;
}
return 0.0;
}
public void SetDragRegion(NonClientRegionKind nonClientRegionKind, params FrameworkElement[] frameworkElements)
{
List<RectInt32> rects = new List<RectInt32>();
var scale = GetRasterizationScaleForElement(this);
foreach (var frameworkElement in frameworkElements)
{
if (frameworkElement == null)
{
continue;
}
GeneralTransform transformElement = frameworkElement.TransformToVisual(null);
Rect bounds = transformElement.TransformBounds(new Rect(0, 0, frameworkElement.ActualWidth, frameworkElement.ActualHeight));
var transparentRect = new RectInt32(
_X: (int)Math.Round(bounds.X * scale),
_Y: (int)Math.Round(bounds.Y * scale),
_Width: (int)Math.Round(bounds.Width * scale),
_Height: (int)Math.Round(bounds.Height * scale));
rects.Add(transparentRect);
}
if (rects.Count > 0)
{
InputNonClientPointerSource.GetForWindowId(Window.AppWindow.Id).SetRegionRects(nonClientRegionKind, rects.ToArray());
}
}
public void ClearDragRegions(NonClientRegionKind nonClientRegionKind)
{
InputNonClientPointerSource.GetForWindowId(Window.AppWindow.Id).ClearRegionRects(nonClientRegionKind);
}
}

View File

@@ -1,220 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Microsoft.PowerToys.Settings.UI.Controls;
[TemplateVisualState(Name = BackButtonVisibleState, GroupName = BackButtonStates)]
[TemplateVisualState(Name = BackButtonCollapsedState, GroupName = BackButtonStates)]
[TemplateVisualState(Name = PaneButtonVisibleState, GroupName = PaneButtonStates)]
[TemplateVisualState(Name = PaneButtonCollapsedState, GroupName = PaneButtonStates)]
[TemplateVisualState(Name = WindowActivatedState, GroupName = ActivationStates)]
[TemplateVisualState(Name = WindowDeactivatedState, GroupName = ActivationStates)]
[TemplateVisualState(Name = StandardState, GroupName = DisplayModeStates)]
[TemplateVisualState(Name = TallState, GroupName = DisplayModeStates)]
[TemplateVisualState(Name = IconVisibleState, GroupName = IconStates)]
[TemplateVisualState(Name = IconCollapsedState, GroupName = IconStates)]
[TemplateVisualState(Name = ContentVisibleState, GroupName = ContentStates)]
[TemplateVisualState(Name = ContentCollapsedState, GroupName = ContentStates)]
[TemplateVisualState(Name = FooterVisibleState, GroupName = FooterStates)]
[TemplateVisualState(Name = FooterCollapsedState, GroupName = FooterStates)]
[TemplateVisualState(Name = WideState, GroupName = ReflowStates)]
[TemplateVisualState(Name = NarrowState, GroupName = ReflowStates)]
[TemplatePart(Name = PartBackButton, Type = typeof(Button))]
[TemplatePart(Name = PartPaneButton, Type = typeof(Button))]
[TemplatePart(Name = nameof(PART_LeftPaddingColumn), Type = typeof(ColumnDefinition))]
[TemplatePart(Name = nameof(PART_RightPaddingColumn), Type = typeof(ColumnDefinition))]
[TemplatePart(Name = nameof(PART_ButtonHolder), Type = typeof(StackPanel))]
public partial class TitleBar : Control
{
private const string PartBackButton = "PART_BackButton";
private const string PartPaneButton = "PART_PaneButton";
private const string BackButtonVisibleState = "BackButtonVisible";
private const string BackButtonCollapsedState = "BackButtonCollapsed";
private const string BackButtonStates = "BackButtonStates";
private const string PaneButtonVisibleState = "PaneButtonVisible";
private const string PaneButtonCollapsedState = "PaneButtonCollapsed";
private const string PaneButtonStates = "PaneButtonStates";
private const string WindowActivatedState = "Activated";
private const string WindowDeactivatedState = "Deactivated";
private const string ActivationStates = "WindowActivationStates";
private const string IconVisibleState = "IconVisible";
private const string IconCollapsedState = "IconCollapsed";
private const string IconStates = "IconStates";
private const string StandardState = "Standard";
private const string TallState = "Tall";
private const string DisplayModeStates = "DisplayModeStates";
private const string ContentVisibleState = "ContentVisible";
private const string ContentCollapsedState = "ContentCollapsed";
private const string ContentStates = "ContentStates";
private const string FooterVisibleState = "FooterVisible";
private const string FooterCollapsedState = "FooterCollapsed";
private const string FooterStates = "FooterStates";
private const string WideState = "Wide";
private const string NarrowState = "Narrow";
private const string ReflowStates = "ReflowStates";
#pragma warning disable SA1306 // Field names should begin with lower-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
#pragma warning disable SA1400 // Access modifier should be declared
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
ColumnDefinition? PART_RightPaddingColumn;
ColumnDefinition? PART_LeftPaddingColumn;
StackPanel? PART_ButtonHolder;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore SA1400 // Access modifier should be declared
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore
// We only want to reset TitleBar configuration in app, if we're the TitleBar instance that's managing that state.
private bool _isAutoConfigCompleted;
public TitleBar()
{
this.DefaultStyleKey = typeof(TitleBar);
}
protected override void OnApplyTemplate()
{
PART_LeftPaddingColumn = GetTemplateChild(nameof(PART_LeftPaddingColumn)) as ColumnDefinition;
PART_RightPaddingColumn = GetTemplateChild(nameof(PART_RightPaddingColumn)) as ColumnDefinition;
ConfigureButtonHolder();
Configure();
if (GetTemplateChild(PartBackButton) is Button backButton)
{
backButton.Click -= BackButton_Click;
backButton.Click += BackButton_Click;
}
if (GetTemplateChild(PartPaneButton) is Button paneButton)
{
paneButton.Click -= PaneButton_Click;
paneButton.Click += PaneButton_Click;
}
SizeChanged -= this.TitleBar_SizeChanged;
SizeChanged += this.TitleBar_SizeChanged;
Update();
base.OnApplyTemplate();
}
private void TitleBar_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateVisualStateAndDragRegion(e.NewSize);
}
private void UpdateVisualStateAndDragRegion(Size size)
{
if (size.Width <= CompactStateBreakpoint)
{
if (Content != null || Footer != null)
{
VisualStateManager.GoToState(this, NarrowState, true);
}
}
else
{
VisualStateManager.GoToState(this, WideState, true);
}
SetDragRegionForCustomTitleBar();
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
BackButtonClick?.Invoke(this, new RoutedEventArgs());
}
private void PaneButton_Click(object sender, RoutedEventArgs e)
{
PaneButtonClick?.Invoke(this, new RoutedEventArgs());
}
private void ConfigureButtonHolder()
{
if (PART_ButtonHolder != null)
{
PART_ButtonHolder.SizeChanged -= PART_ButtonHolder_SizeChanged;
}
PART_ButtonHolder = GetTemplateChild(nameof(PART_ButtonHolder)) as StackPanel;
if (PART_ButtonHolder != null)
{
PART_ButtonHolder.SizeChanged += PART_ButtonHolder_SizeChanged;
}
}
private void PART_ButtonHolder_SizeChanged(object sender, SizeChangedEventArgs e)
{
SetDragRegionForCustomTitleBar();
}
private void Configure()
{
SetWASDKTitleBar();
}
public void Reset()
{
ResetWASDKTitleBar();
}
private void Update()
{
if (Icon != null)
{
VisualStateManager.GoToState(this, IconVisibleState, true);
}
else
{
VisualStateManager.GoToState(this, IconCollapsedState, true);
}
VisualStateManager.GoToState(this, IsBackButtonVisible ? BackButtonVisibleState : BackButtonCollapsedState, true);
VisualStateManager.GoToState(this, IsPaneButtonVisible ? PaneButtonVisibleState : PaneButtonCollapsedState, true);
if (DisplayMode == DisplayMode.Tall)
{
VisualStateManager.GoToState(this, TallState, true);
}
else
{
VisualStateManager.GoToState(this, StandardState, true);
}
if (Content != null)
{
VisualStateManager.GoToState(this, ContentVisibleState, true);
}
else
{
VisualStateManager.GoToState(this, ContentCollapsedState, true);
}
if (Footer != null)
{
VisualStateManager.GoToState(this, FooterVisibleState, true);
}
else
{
VisualStateManager.GoToState(this, FooterCollapsedState, true);
}
SetDragRegionForCustomTitleBar();
}
}

View File

@@ -1,371 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls">
<x:Double x:Key="TitleBarCompactHeight">32</x:Double>
<x:Double x:Key="TitleBarTallHeight">48</x:Double>
<x:Double x:Key="TitleBarContentMinWidth">360</x:Double>
<Style BasedOn="{StaticResource DefaultTitleBarStyle}" TargetType="local:TitleBar" />
<Style x:Key="DefaultTitleBarStyle" TargetType="local:TitleBar">
<Setter Property="MinHeight" Value="{ThemeResource TitleBarCompactHeight}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TitleBar">
<Grid
x:Name="PART_RootGrid"
Height="{TemplateBinding MinHeight}"
Padding="4,0,0,0"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="PART_LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="PART_ButtonsHolderColumn" Width="Auto" />
<ColumnDefinition x:Name="PART_IconColumn" Width="Auto" />
<ColumnDefinition x:Name="PART_TitleColumn" Width="Auto" />
<ColumnDefinition
x:Name="PART_LeftDragColumn"
Width="*"
MinWidth="4" />
<ColumnDefinition x:Name="PART_ContentColumn" Width="Auto" />
<ColumnDefinition
x:Name="PART_RightDragColumn"
Width="*"
MinWidth="4" />
<ColumnDefinition x:Name="PART_FooterColumn" Width="Auto" />
<ColumnDefinition x:Name="PART_RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Border
x:Name="PART_IconHolder"
Grid.Column="2"
Margin="12,0,0,0"
VerticalAlignment="Center">
<Viewbox
x:Name="PART_Icon"
MaxWidth="16"
MaxHeight="16">
<ContentPresenter
x:Name="PART_IconPresenter"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Icon}"
HighContrastAdjustment="None" />
</Viewbox>
</Border>
<StackPanel
x:Name="PART_TitleHolder"
Grid.Column="3"
Margin="16,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="4">
<TextBlock
x:Name="PART_TitleText"
MinWidth="48"
Margin="0,0,0,1"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Title}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="PART_SubtitleText"
MinWidth="48"
Margin="0,0,0,1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Subtitle}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
<Grid
x:Name="PART_DragRegion"
Grid.Column="2"
Grid.ColumnSpan="6"
Background="Transparent" />
<StackPanel
x:Name="PART_ButtonHolder"
Grid.Column="1"
Orientation="Horizontal">
<Button
x:Name="PART_BackButton"
Style="{ThemeResource TitleBarBackButtonStyle}"
ToolTipService.ToolTip="Back" />
<Button
x:Name="PART_PaneButton"
Style="{StaticResource TitleBarPaneToggleButtonStyle}"
ToolTipService.ToolTip="Toggle menu" />
</StackPanel>
<ContentPresenter
x:Name="PART_ContentPresenter"
Grid.Column="5"
MinWidth="{ThemeResource TitleBarContentMinWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
HorizontalContentAlignment="Stretch"
Content="{TemplateBinding Content}" />
<ContentPresenter
x:Name="PART_FooterPresenter"
Grid.Column="7"
Margin="4,0,8,0"
HorizontalContentAlignment="Right"
Content="{TemplateBinding Footer}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="BackButtonStates">
<VisualState x:Name="BackButtonVisible" />
<VisualState x:Name="BackButtonCollapsed">
<VisualState.Setters>
<Setter Target="PART_BackButton.Visibility" Value="Collapsed" />
<Setter Target="PART_BackButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="PaneButtonStates">
<VisualState x:Name="PaneButtonVisible" />
<VisualState x:Name="PaneButtonCollapsed">
<VisualState.Setters>
<Setter Target="PART_PaneButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="IconStates">
<VisualState x:Name="IconVisible" />
<VisualState x:Name="IconCollapsed">
<VisualState.Setters>
<Setter Target="PART_IconHolder.Visibility" Value="Collapsed" />
<Setter Target="PART_TitleHolder.Margin" Value="4,0,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ContentStates">
<VisualState x:Name="ContentVisible" />
<VisualState x:Name="ContentCollapsed">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FooterStates">
<VisualState x:Name="FooterVisible" />
<VisualState x:Name="FooterCollapsed">
<VisualState.Setters>
<Setter Target="PART_FooterPresenter.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ReflowStates">
<VisualState x:Name="Wide" />
<VisualState x:Name="Narrow">
<VisualState.Setters>
<Setter Target="PART_TitleHolder.Visibility" Value="Collapsed" />
<Setter Target="PART_LeftDragColumn.Width" Value="Auto" />
<Setter Target="PART_RightDragColumn.Width" Value="Auto" />
<Setter Target="PART_RightDragColumn.MinWidth" Value="16" />
<Setter Target="PART_LeftDragColumn.MinWidth" Value="16" />
<Setter Target="PART_ContentColumn.Width" Value="*" />
<!-- Content can stretch now -->
<Setter Target="PART_ContentPresenter.MinWidth" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="WindowActivationStates">
<VisualState x:Name="Activated" />
<VisualState x:Name="Deactivated">
<VisualState.Setters>
<Setter Target="PART_TitleText.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="PART_SubtitleText.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="PART_BackButton.IsEnabled" Value="False" />
<Setter Target="PART_PaneButton.IsEnabled" Value="False" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DisplayModeStates">
<VisualState x:Name="Standard" />
<VisualState x:Name="Tall">
<VisualState.Setters>
<Setter Target="PART_RootGrid.MinHeight" Value="{ThemeResource TitleBarTallHeight}" />
<Setter Target="PART_RootGrid.Padding" Value="4" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Copy of WinUI NavigationBackButtonNormalStyle - cannot use it as it picks up the generic.xaml version, not the WinUI version -->
<Style x:Key="TitleBarBackButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource NavigationViewBackButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource NavigationViewItemForegroundChecked}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="MaxHeight" Value="40" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Content" Value="&#xE72B;" />
<Setter Property="Padding" Value="12,4,12,4" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid
x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<AnimatedIcon
x:Name="Content"
Width="16"
Height="16"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw">
<animatedvisuals:AnimatedBackVisualSource />
<AnimatedIcon.FallbackIconSource>
<FontIconSource
FontFamily="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontFamily}"
FontSize="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontSize}"
Glyph="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}"
MirroredWhenRightToLeft="True" />
</AnimatedIcon.FallbackIconSource>
</AnimatedIcon>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Copy of WinUI PaneToggleButtonStyle - cannot use it as it picks up the generic.xaml version, not the WinUI version -->
<Style x:Key="TitleBarPaneToggleButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource NavigationViewBackButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource NavigationViewItemForegroundChecked}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="MaxHeight" Value="40" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Content" Value="&#xE700;" />
<Setter Property="Padding" Value="12,4,12,4" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid
x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<AnimatedIcon
x:Name="Content"
Width="16"
Height="16"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw">
<animatedvisuals:AnimatedGlobalNavigationButtonVisualSource />
<AnimatedIcon.FallbackIconSource>
<FontIconSource
FontFamily="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontFamily}"
FontSize="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontSize}"
Glyph="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}"
MirroredWhenRightToLeft="True" />
</AnimatedIcon.FallbackIconSource>
</AnimatedIcon>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -24,7 +24,6 @@
<controls:SettingsGroup x:Uid="MouseUtils_MouseJump">
<tkcontrols:SettingsCard
x:Name="MouseUtilsEnableMouseJump"
x:Uid="MouseUtils_Enable_MouseJump"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseJump.png}"
IsEnabled="{x:Bind ViewModel.IsJumpEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
@@ -43,7 +42,6 @@
</InfoBar>
<tkcontrols:SettingsCard
x:Name="MouseUtilsMouseJumpActivationShortcut"
x:Uid="MouseUtils_MouseJump_ActivationShortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsEnabled="{x:Bind ViewModel.IsMouseJumpEnabled, Mode=OneWay}">
@@ -128,7 +126,6 @@
</tkcontrols:SettingsCard>
<tkcontrols:SettingsExpander
x:Name="MouseUtilsMouseJumpAppearance"
x:Uid="MouseUtils_MouseJump_Appearance"
HeaderIcon="{ui:FontIcon Glyph=&#xEB3C;}"
IsEnabled="{x:Bind ViewModel.IsMouseJumpEnabled, Mode=OneWay}"

View File

@@ -1,4 +1,4 @@
<local:NavigablePage
<local:NavigatablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.AdvancedPastePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -12,7 +12,7 @@
x:Name="RootPage"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<local:NavigablePage.Resources>
<local:NavigatablePage.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
@@ -38,7 +38,7 @@
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</local:NavigablePage.Resources>
</local:NavigatablePage.Resources>
<Grid>
<controls:SettingsPageControl x:Uid="AdvancedPaste" ModuleImageSource="ms-appx:///Assets/Settings/Modules/AdvancedPaste.png">
<controls:SettingsPageControl.ModuleContent>
@@ -425,4 +425,4 @@
</StackPanel>
</ContentDialog>
</Grid>
</local:NavigablePage>
</local:NavigatablePage>

View File

@@ -15,7 +15,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class AdvancedPastePage : NavigablePage, IRefreshablePage
public sealed partial class AdvancedPastePage : NavigatablePage, IRefreshablePage
{
private AdvancedPasteViewModel ViewModel { get; set; }

View File

@@ -1,4 +1,4 @@
<local:NavigablePage
<local:NavigatablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.AlwaysOnTopPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -136,4 +136,4 @@
<controls:PageLink x:Uid="LearnMore_AlwaysOnTop" Link="https://aka.ms/PowerToysOverview_AlwaysOnTop" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>
</local:NavigatablePage>

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class AlwaysOnTopPage : NavigablePage, IRefreshablePage
public sealed partial class AlwaysOnTopPage : NavigatablePage, IRefreshablePage
{
private AlwaysOnTopViewModel ViewModel { get; set; }

View File

@@ -1,4 +1,4 @@
<local:NavigablePage
<local:NavigatablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.AwakePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -14,9 +14,9 @@
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<local:NavigablePage.Resources>
<local:NavigatablePage.Resources>
<converters:AwakeModeToIntConverter x:Key="AwakeModeToIntConverter" />
</local:NavigablePage.Resources>
</local:NavigatablePage.Resources>
<controls:SettingsPageControl
x:Uid="Awake"
@@ -119,4 +119,4 @@
<controls:PageLink x:Uid="SecondaryLink_Awake" Link="https://awake.den.dev" />
</controls:SettingsPageControl.SecondaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>
</local:NavigatablePage>

View File

@@ -17,7 +17,7 @@ using PowerToys.GPOWrapper;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class AwakePage : NavigablePage, IRefreshablePage
public sealed partial class AwakePage : NavigatablePage, IRefreshablePage
{
private readonly string _appName = "Awake";
private readonly SettingsUtils _settingsUtils;

View File

@@ -1,4 +1,4 @@
<local:NavigablePage
<local:NavigatablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.CmdNotFoundPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -170,4 +170,4 @@
<controls:PageLink x:Uid="LearnMore_CmdNotFound" Link="https://aka.ms/PowerToysOverview_CmdNotFound" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>
</local:NavigatablePage>

View File

@@ -8,7 +8,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class CmdNotFoundPage : NavigablePage
public sealed partial class CmdNotFoundPage : NavigatablePage
{
private CmdNotFoundViewModel ViewModel { get; set; }

View File

@@ -1,4 +1,4 @@
<local:NavigablePage
<local:NavigatablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.CmdPalPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -52,4 +52,4 @@
<controls:PageLink x:Uid="LearnMore_CmdPal" Link="https://aka.ms/PowerToysOverview_CmdPal" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>
</local:NavigatablePage>

View File

@@ -13,7 +13,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class CmdPalPage : NavigablePage, IRefreshablePage
public sealed partial class CmdPalPage : NavigatablePage, IRefreshablePage
{
private CmdPalViewModel ViewModel { get; set; }

View File

@@ -1,4 +1,4 @@
<local:NavigablePage
<local:NavigatablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.ColorPickerPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -232,4 +232,4 @@
<controls:PageLink Link="https://medium.com/@Niels9001/a-fluent-color-meter-for-powertoys-20407ededf0c" Text="Niels Laute's UX concept" />
</controls:SettingsPageControl.SecondaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>
</local:NavigatablePage>

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