mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 19:57:57 +01:00
Merge remote-tracking branch 'origin/main' into dev/snickler/net10-upgrade
This commit is contained in:
14
.github/actions/spell-check/expect.txt
vendored
14
.github/actions/spell-check/expect.txt
vendored
@@ -49,6 +49,7 @@ ALPHATYPE
|
|||||||
AModifier
|
AModifier
|
||||||
amr
|
amr
|
||||||
ANDSCANS
|
ANDSCANS
|
||||||
|
animatedvisuals
|
||||||
Animnate
|
Animnate
|
||||||
ANull
|
ANull
|
||||||
AOC
|
AOC
|
||||||
@@ -69,6 +70,7 @@ APPMODEL
|
|||||||
APPNAME
|
APPNAME
|
||||||
appref
|
appref
|
||||||
appsettings
|
appsettings
|
||||||
|
appsfeatures
|
||||||
appwindow
|
appwindow
|
||||||
appwiz
|
appwiz
|
||||||
appxpackage
|
appxpackage
|
||||||
@@ -304,6 +306,7 @@ CXVIRTUALSCREEN
|
|||||||
CYSCREEN
|
CYSCREEN
|
||||||
CYSMICON
|
CYSMICON
|
||||||
CYVIRTUALSCREEN
|
CYVIRTUALSCREEN
|
||||||
|
Czechia
|
||||||
cziplib
|
cziplib
|
||||||
Dac
|
Dac
|
||||||
dacl
|
dacl
|
||||||
@@ -328,6 +331,7 @@ Deact
|
|||||||
debugbreak
|
debugbreak
|
||||||
decryptor
|
decryptor
|
||||||
Dedup
|
Dedup
|
||||||
|
Deduplicator
|
||||||
Deeplink
|
Deeplink
|
||||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||||
DEFAULTCOLOR
|
DEFAULTCOLOR
|
||||||
@@ -433,6 +437,7 @@ EDITSHORTCUTS
|
|||||||
EDITTEXT
|
EDITTEXT
|
||||||
EFile
|
EFile
|
||||||
ekus
|
ekus
|
||||||
|
emojis
|
||||||
ENABLEDELAYEDEXPANSION
|
ENABLEDELAYEDEXPANSION
|
||||||
ENABLEDPOPUP
|
ENABLEDPOPUP
|
||||||
ENABLETAB
|
ENABLETAB
|
||||||
@@ -797,6 +802,7 @@ KEYBOARDMANAGEREDITORLIBRARYWRAPPER
|
|||||||
keyboardmanagerstate
|
keyboardmanagerstate
|
||||||
keyboardmanagerui
|
keyboardmanagerui
|
||||||
keyboardtester
|
keyboardtester
|
||||||
|
keycap
|
||||||
KEYEVENTF
|
KEYEVENTF
|
||||||
KEYIMAGE
|
KEYIMAGE
|
||||||
keynum
|
keynum
|
||||||
@@ -1024,8 +1030,6 @@ MYICON
|
|||||||
NAMECHANGE
|
NAMECHANGE
|
||||||
namespaceanddescendants
|
namespaceanddescendants
|
||||||
nao
|
nao
|
||||||
Navigatable
|
|
||||||
NavigatablePage
|
|
||||||
NCACTIVATE
|
NCACTIVATE
|
||||||
ncc
|
ncc
|
||||||
NCCALCSIZE
|
NCCALCSIZE
|
||||||
@@ -1316,6 +1320,7 @@ PRODUCTVERSION
|
|||||||
Progman
|
Progman
|
||||||
programdata
|
programdata
|
||||||
projectname
|
projectname
|
||||||
|
projitems
|
||||||
PROPERTYKEY
|
PROPERTYKEY
|
||||||
Propset
|
Propset
|
||||||
PROPVARIANT
|
PROPVARIANT
|
||||||
@@ -1395,6 +1400,7 @@ regkey
|
|||||||
regroot
|
regroot
|
||||||
regsvr
|
regsvr
|
||||||
REINSTALLMODE
|
REINSTALLMODE
|
||||||
|
releaseblog
|
||||||
reloadable
|
reloadable
|
||||||
Relogger
|
Relogger
|
||||||
remappings
|
remappings
|
||||||
@@ -1778,10 +1784,13 @@ UACUI
|
|||||||
UAL
|
UAL
|
||||||
uap
|
uap
|
||||||
UBR
|
UBR
|
||||||
|
UBreak
|
||||||
|
ubrk
|
||||||
UCallback
|
UCallback
|
||||||
ucrt
|
ucrt
|
||||||
ucrtd
|
ucrtd
|
||||||
uefi
|
uefi
|
||||||
|
UError
|
||||||
uesc
|
uesc
|
||||||
UFlags
|
UFlags
|
||||||
UHash
|
UHash
|
||||||
@@ -1851,6 +1860,7 @@ VFT
|
|||||||
vget
|
vget
|
||||||
vgetq
|
vgetq
|
||||||
viewmodels
|
viewmodels
|
||||||
|
virama
|
||||||
VIRTKEY
|
VIRTKEY
|
||||||
VIRTUALDESK
|
VIRTUALDESK
|
||||||
VISEGRADRELAY
|
VISEGRADRELAY
|
||||||
|
|||||||
4
.github/actions/spell-check/patterns.txt
vendored
4
.github/actions/spell-check/patterns.txt
vendored
@@ -260,3 +260,7 @@ Process Process
|
|||||||
# ZoomIt menu items with accelerator keys
|
# ZoomIt menu items with accelerator keys
|
||||||
E&xit
|
E&xit
|
||||||
St&yle
|
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
|
||||||
|
|||||||
38
.github/workflows/manual-batch-issue-deduplication.yml
vendored
Normal file
38
.github/workflows/manual-batch-issue-deduplication.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Manual Batch Issue Deduplication
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
issue_numbers:
|
||||||
|
description: "JSON array of issue numbers to deduplicate (e.g. [101,102,103])"
|
||||||
|
required: true
|
||||||
|
since:
|
||||||
|
description: "Only compare against issues created after this date (ISO 8601, e.g. 2019-05-05T00:00:00Z)"
|
||||||
|
required: false
|
||||||
|
default: "2019-05-05T00:00:00Z"
|
||||||
|
label_as_duplicate:
|
||||||
|
description: "Apply duplicate label if duplicates are found (true/false)"
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
models: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deduplicate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run GenAI Issue Deduplicator
|
||||||
|
uses: pelikhan/action-genai-issue-dedup@v0
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
github_issue: ${{ matrix.issue }}
|
||||||
|
label_as_duplicate: ${{ github.event.inputs.label_as_duplicate }}
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
<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.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.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="ControlzEx" Version="6.0.0" />
|
||||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||||
|
|||||||
@@ -1499,7 +1499,6 @@ SOFTWARE.
|
|||||||
- CoenM.ImageSharp.ImageHash
|
- CoenM.ImageSharp.ImageHash
|
||||||
- CommunityToolkit.Common
|
- CommunityToolkit.Common
|
||||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||||
- CommunityToolkit.Labs.WinUI.TitleBar
|
|
||||||
- CommunityToolkit.Mvvm
|
- CommunityToolkit.Mvvm
|
||||||
- CommunityToolkit.WinUI.Animations
|
- CommunityToolkit.WinUI.Animations
|
||||||
- CommunityToolkit.WinUI.Collections
|
- CommunityToolkit.WinUI.Collections
|
||||||
|
|||||||
199
README.md
199
README.md
@@ -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.
|
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 -->
|
<!-- 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.94%22
|
[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.93%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.93.0/PowerToysUserSetup-0.93.0-x64.exe
|
[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.93.0/PowerToysUserSetup-0.93.0-arm64.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.93.0/PowerToysSetup-0.93.0-x64.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.93.0/PowerToysSetup-0.93.0-arm64.exe
|
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
|
||||||
|
|
||||||
| Description | Filename |
|
| Description | Filename |
|
||||||
|----------------|----------|
|
|----------------|----------|
|
||||||
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] |
|
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
|
||||||
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] |
|
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
|
||||||
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] |
|
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
|
||||||
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] |
|
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
|
||||||
|
|
||||||
This is our preferred method.
|
This is our preferred method.
|
||||||
|
|
||||||
@@ -93,118 +93,145 @@ 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.
|
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
|
||||||
|
|
||||||
### 0.93 - Aug 2025 Update
|
### 0.94 - Sep 2025 Update
|
||||||
|
|
||||||
In this release, we focused on new features, stability, optimization improvements, and automation.
|
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**
|
**✨Highlights**
|
||||||
|
|
||||||
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience.
|
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
|
||||||
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run.
|
- 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.
|
||||||
- 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.
|
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for single‑button cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||||
- 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)!
|
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
|
||||||
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations.
|
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
|
||||||
- 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.
|
- 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)!
|
||||||
|
|
||||||
### Command Palette
|
### Command Palette
|
||||||
|
|
||||||
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
|
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed command title changes not being properly notified to screen readers. 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 icon controls excluded from keyboard navigation by default for better accessibility. 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)!
|
||||||
- Improved UI design with better text sizing and alignment.
|
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed keyboard shortcuts to work better in text boxes and context menus.
|
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Added right-click context menus with critical command styling and separators.
|
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality.
|
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed context menu crashes with better type handling.
|
- Standardized null checks using C# pattern matching for safer behavior.
|
||||||
- Fixed "Reload" command to work with both uppercase and lowercase letters.
|
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||||
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
|
||||||
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||||
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
|
||||||
- Fixed window focus not returning to previous app properly.
|
- Added single-select filters to DynamicListPage and updated Windows Services sample.
|
||||||
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
|
||||||
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Propagated alias changes safely and resolved conflicts across view models.
|
||||||
- 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)!
|
- Allowed providers to override Dispose with a virtual method.
|
||||||
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Fixed memory leaks by cleaning up removed or cancelled list items.
|
||||||
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)!
|
- Sorted DateTime extension results by relevance for better usability.
|
||||||
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- 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.
|
||||||
|
|
||||||
### Command Palette extensions
|
### Command Palette extensions
|
||||||
|
|
||||||
- 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.
|
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||||
- 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).
|
- Added app icons to the All Apps "Run" context command when available.
|
||||||
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)!
|
- Restored missing builtin icons by standardizing extension dependencies.
|
||||||
- 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)!
|
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
|
||||||
- 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)!
|
### Hosts File Editor
|
||||||
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
|
|
||||||
- Added keyboard shortcuts to the *Apps* context menu items for faster access.
|
- 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)!
|
||||||
- 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.
|
### Image Resizer
|
||||||
- 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 Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
|
||||||
- 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
|
### Mouse Utilities
|
||||||
|
|
||||||
- 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.
|
- 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)!
|
||||||
|
|
||||||
### Peek
|
### Peek
|
||||||
|
|
||||||
- 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)!
|
- 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)!
|
||||||
|
|
||||||
### Quick Accent
|
### Quick Accent
|
||||||
|
|
||||||
- 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)!
|
- 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)!
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
- 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.
|
- Added telemetry to track usage of the new shortcut conflict detection workflow.
|
||||||
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand.
|
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||||
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing.
|
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
|
||||||
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings.
|
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
|
||||||
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window.
|
- Introduced Settings search with fuzzy matching and navigation to specific controls.
|
||||||
- Resolved a settings crash related to incompatible property names in ZoomIt configuration.
|
- 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.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- 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)!
|
- Adds docs for building the installer locally and testing winget installs.
|
||||||
- **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)!
|
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
|
||||||
- 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
|
### Development
|
||||||
|
|
||||||
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)!
|
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
|
||||||
- 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)!
|
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
|
||||||
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother.
|
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
|
||||||
- Replaced NuGet feed with Azure Artifacts for better package management.
|
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
|
||||||
- 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.
|
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
|
||||||
- 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.
|
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
|
||||||
- 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.
|
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||||
- 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 accessibility IDs to CmdPal UI for stable UI tests.
|
||||||
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality.
|
- Rewrote system command tests with a new test base and cleaner patterns.
|
||||||
- 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.
|
- Added unit tests for WebSearch and Shell extensions with mockable settings.
|
||||||
|
- Added unit tests and abstractions for Apps and Bookmarks extensions.
|
||||||
|
- Cleans up AI‑generated tests; adds meaningful query tests across extensions.
|
||||||
|
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
|
||||||
|
|
||||||
### What is being planned over the next few releases
|
### What is being planned over the next few releases
|
||||||
|
|
||||||
For [v0.94][github-next-release-work], we'll work on the items below:
|
For [v0.95][github-next-release-work], we'll work on the items below:
|
||||||
|
|
||||||
- Continued Command Palette polish
|
- Continued Command Palette polish
|
||||||
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
|
- 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
|
- Upgrading Keyboard Manager's editor UI
|
||||||
|
- UI tweaking utility with day/night theme switcher
|
||||||
|
- DSC v3 support for top utilities
|
||||||
- New UI automation tests
|
- New UI automation tests
|
||||||
- Stability, bug fixes
|
- Stability, bug fixes
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
|||||||
1. Windows 10 April 2018 Update (version 1803) or newer
|
1. Windows 10 April 2018 Update (version 1803) or newer
|
||||||
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer
|
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer
|
||||||
1. A local clone of the PowerToys repository
|
1. A local clone of the PowerToys repository
|
||||||
|
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
|
||||||
|
|
||||||
### Install Visual Studio dependencies
|
### Install Visual Studio dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -32,17 +32,8 @@ namespace ManagedCommon
|
|||||||
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
|
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
|
||||||
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
|
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
|
||||||
{
|
{
|
||||||
string basePath;
|
string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
|
||||||
if (isLocalLow)
|
string basePath = Path.GetDirectoryName(versionedPath);
|
||||||
{
|
|
||||||
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
basePath = Constants.AppDataPath() + applicationLogPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
string versionedPath = Path.Combine(basePath, Version);
|
|
||||||
|
|
||||||
if (!Directory.Exists(versionedPath))
|
if (!Directory.Exists(versionedPath))
|
||||||
{
|
{
|
||||||
@@ -59,6 +50,22 @@ namespace ManagedCommon
|
|||||||
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
|
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>
|
/// <summary>
|
||||||
/// Deletes old version log folders, keeping only the current version's folder.
|
/// Deletes old version log folders, keeping only the current version's folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -115,13 +122,13 @@ namespace ManagedCommon
|
|||||||
{
|
{
|
||||||
var exMessage =
|
var exMessage =
|
||||||
message + Environment.NewLine +
|
message + Environment.NewLine +
|
||||||
ex.GetType() + ": " + ex.Message + Environment.NewLine;
|
ex.GetType() + " (" + ex.HResult + "): " + ex.Message + Environment.NewLine;
|
||||||
|
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
exMessage +=
|
exMessage +=
|
||||||
"Inner exception: " + Environment.NewLine +
|
"Inner exception: " + Environment.NewLine +
|
||||||
ex.InnerException.GetType() + ": " + ex.InnerException.Message + Environment.NewLine;
|
ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
exMessage +=
|
exMessage +=
|
||||||
|
|||||||
@@ -20,27 +20,14 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid
|
<TitleBar x:Name="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 -->
|
||||||
Height="32"
|
<TitleBar.LeftHeader>
|
||||||
ColumnSpacing="16">
|
<ImageIcon
|
||||||
<Grid.ColumnDefinitions>
|
Height="16"
|
||||||
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
|
Margin="16,0,0,0"
|
||||||
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
|
Source="/Assets/EnvironmentVariables/EnvironmentVariables.ico" />
|
||||||
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
|
</TitleBar.LeftHeader>
|
||||||
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
|
</TitleBar>
|
||||||
</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>
|
</Grid>
|
||||||
</winuiex:WindowEx>
|
</winuiex:WindowEx>
|
||||||
|
|||||||
@@ -4,22 +4,19 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
using EnvironmentVariables.Win32;
|
using EnvironmentVariables.Win32;
|
||||||
using EnvironmentVariablesUILib;
|
using EnvironmentVariablesUILib;
|
||||||
using EnvironmentVariablesUILib.Helpers;
|
using EnvironmentVariablesUILib.Helpers;
|
||||||
using EnvironmentVariablesUILib.ViewModels;
|
using EnvironmentVariablesUILib.ViewModels;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Windowing;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using WinUIEx;
|
using WinUIEx;
|
||||||
|
|
||||||
namespace EnvironmentVariables
|
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
|
public sealed partial class MainWindow : WindowEx
|
||||||
{
|
{
|
||||||
private EnvironmentVariablesMainPage MainPage { get; }
|
private EnvironmentVariablesMainPage MainPage { get; }
|
||||||
@@ -34,8 +31,9 @@ namespace EnvironmentVariables
|
|||||||
AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
|
AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
|
||||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||||
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||||
|
|
||||||
Title = title;
|
Title = title;
|
||||||
AppTitleTextBlock.Text = title;
|
titleBar.Title = title;
|
||||||
|
|
||||||
var handle = this.GetWindowHandle();
|
var handle = this.GetWindowHandle();
|
||||||
RegisterWindow(handle);
|
RegisterWindow(handle);
|
||||||
|
|||||||
@@ -19,6 +19,26 @@
|
|||||||
|
|
||||||
class FileLocksmithModule : public PowertoyModuleIface
|
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:
|
public:
|
||||||
FileLocksmithModule()
|
FileLocksmithModule()
|
||||||
{
|
{
|
||||||
@@ -88,21 +108,16 @@ public:
|
|||||||
package::RegisterSparsePackage(path, packageUri);
|
package::RegisterSparsePackage(path, packageUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
|
||||||
FileLocksmithRuntimeRegistration::EnsureRegistered();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
m_enabled = true;
|
m_enabled = true;
|
||||||
|
UpdateRegistration(m_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void disable() override
|
virtual void disable() override
|
||||||
{
|
{
|
||||||
Logger::info(L"File Locksmith disabled");
|
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;
|
m_enabled = false;
|
||||||
|
UpdateRegistration(m_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool is_enabled() override
|
virtual bool is_enabled() override
|
||||||
@@ -135,6 +150,7 @@ private:
|
|||||||
{
|
{
|
||||||
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
|
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
|
||||||
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
|
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
|
||||||
|
UpdateRegistration(m_enabled);
|
||||||
Trace::EnableFileLocksmith(m_enabled);
|
Trace::EnableFileLocksmith(m_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,30 +20,15 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid
|
<TitleBar x:Name="titleBar">
|
||||||
x:Name="AppTitleBar"
|
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||||
Height="32"
|
<TitleBar.LeftHeader>
|
||||||
ColumnSpacing="16">
|
<ImageIcon
|
||||||
<Grid.ColumnDefinitions>
|
Height="16"
|
||||||
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
|
Margin="16,0,0,0"
|
||||||
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
|
Source="/Assets/FileLocksmith/Icon.ico" />
|
||||||
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
|
</TitleBar.LeftHeader>
|
||||||
<ColumnDefinition x:Name="RightDragColumn" Width="*" />
|
</TitleBar>
|
||||||
<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" />
|
<views:MainPage x:Name="mainPage" Grid.Row="1" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</winuiex:WindowEx>
|
</winuiex:WindowEx>
|
||||||
@@ -18,30 +18,16 @@ namespace FileLocksmithUI
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
mainPage.ViewModel.IsElevated = isElevated;
|
mainPage.ViewModel.IsElevated = isElevated;
|
||||||
|
SetTitleBar(titleBar);
|
||||||
ExtendsContentIntoTitleBar = true;
|
ExtendsContentIntoTitleBar = true;
|
||||||
SetTitleBar(AppTitleBar);
|
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
|
||||||
Activated += MainWindow_Activated;
|
|
||||||
AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico");
|
AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico");
|
||||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
|
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
|
||||||
|
|
||||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||||
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
|
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
|
||||||
Title = title;
|
Title = title;
|
||||||
AppTitleTextBlock.Text = title;
|
titleBar.Title = 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()
|
public void Dispose()
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</ContentDialog>
|
</ContentDialog>
|
||||||
<ContentDialog x:Name="ProcessFilesListDialog" x:Uid="ProcessFilesListDialog">
|
<ContentDialog x:Name="ProcessFilesListDialog" x:Uid="ProcessFilesListDialog">
|
||||||
<ScrollViewer Padding="15" HorizontalScrollBarVisibility="Auto">
|
<ScrollViewer Padding="16" HorizontalScrollBarVisibility="Auto">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="ProcessFilesListDialogTextBlock"
|
x:Name="ProcessFilesListDialogTextBlock"
|
||||||
x:Uid="ProcessFilesListDialogTextBlock"
|
x:Uid="ProcessFilesListDialogTextBlock"
|
||||||
|
|||||||
@@ -20,27 +20,14 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid
|
<TitleBar x:Name="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 -->
|
||||||
Height="32"
|
<TitleBar.LeftHeader>
|
||||||
ColumnSpacing="16">
|
<ImageIcon
|
||||||
<Grid.ColumnDefinitions>
|
Height="16"
|
||||||
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
|
Margin="16,0,0,0"
|
||||||
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
|
Source="/Assets/Hosts/Hosts.ico" />
|
||||||
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
|
</TitleBar.LeftHeader>
|
||||||
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
|
</TitleBar>
|
||||||
</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>
|
</Grid>
|
||||||
</winuiex:WindowEx>
|
</winuiex:WindowEx>
|
||||||
|
|||||||
@@ -9,19 +9,15 @@ using HostsUILib.Helpers;
|
|||||||
using HostsUILib.Views;
|
using HostsUILib.Views;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
|
using Microsoft.UI.Windowing;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Microsoft.Windows.ApplicationModel.Resources;
|
using Microsoft.Windows.ApplicationModel.Resources;
|
||||||
using WinUIEx;
|
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
|
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
|
public sealed partial class MainWindow : WindowEx
|
||||||
{
|
{
|
||||||
private HostsMainPage MainPage { get; }
|
private HostsMainPage MainPage { get; }
|
||||||
@@ -38,31 +34,18 @@ namespace Hosts
|
|||||||
|
|
||||||
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||||
Title = title;
|
Title = title;
|
||||||
AppTitleTextBlock.Text = title;
|
titleBar.Title = title;
|
||||||
|
|
||||||
var handle = this.GetWindowHandle();
|
var handle = this.GetWindowHandle();
|
||||||
|
|
||||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(handle);
|
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(handle);
|
||||||
WindowHelpers.BringToForeground(handle);
|
WindowHelpers.BringToForeground(handle);
|
||||||
Activated += MainWindow_Activated;
|
|
||||||
|
|
||||||
MainPage = Host.GetService<HostsMainPage>();
|
MainPage = Host.GetService<HostsMainPage>();
|
||||||
|
|
||||||
PowerToysTelemetry.Log.WriteEvent(new HostEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
|
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)
|
private void Grid_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
MainGrid.Children.Add(MainPage);
|
MainGrid.Children.Add(MainPage);
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ struct CommonState
|
|||||||
|
|
||||||
Measurement::Unit units = Measurement::Unit::Pixel;
|
Measurement::Unit units = Measurement::Unit::Pixel;
|
||||||
|
|
||||||
POINT cursorPosSystemSpace = {}; // updated atomically
|
#pragma warning(push)
|
||||||
|
#pragma warning(disable : 4324)
|
||||||
|
alignas(8) POINT cursorPosSystemSpace = {}; // updated atomically
|
||||||
|
#pragma warning(pop)
|
||||||
|
|
||||||
std::atomic_bool closeOnOtherMonitors = false;
|
std::atomic_bool closeOnOtherMonitors = false;
|
||||||
|
|
||||||
float GetPhysicalPx2MmRatio(HWND window) const
|
float GetPhysicalPx2MmRatio(HWND window) const
|
||||||
|
|||||||
@@ -21,6 +21,26 @@
|
|||||||
// Note: Settings are managed via Settings and UI Settings
|
// Note: Settings are managed via Settings and UI Settings
|
||||||
class NewModule : public PowertoyModuleIface
|
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:
|
public:
|
||||||
NewModule()
|
NewModule()
|
||||||
{
|
{
|
||||||
@@ -98,14 +118,9 @@ public:
|
|||||||
{
|
{
|
||||||
newplus::utilities::register_msix_package();
|
newplus::utilities::register_msix_package();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
|
||||||
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
powertoy_new_enabled = true;
|
powertoy_new_enabled = true;
|
||||||
|
UpdateRegistration(powertoy_new_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void disable() override
|
virtual void disable() override
|
||||||
@@ -150,19 +165,14 @@ private:
|
|||||||
{
|
{
|
||||||
Trace::EventToggleOnOff(false);
|
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;
|
powertoy_new_enabled = false;
|
||||||
|
UpdateRegistration(powertoy_new_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void init_settings()
|
void init_settings()
|
||||||
{
|
{
|
||||||
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
|
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
|
||||||
|
UpdateRegistration(powertoy_new_enabled);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ namespace Awake.Core
|
|||||||
|
|
||||||
private static DateTimeOffset ExpireAt { get; set; }
|
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 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 CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
|
||||||
private static readonly BlockingCollection<ExecutionState> _stateQueue;
|
private static readonly BlockingCollection<ExecutionState> _stateQueue;
|
||||||
private static CancellationTokenSource _tokenSource;
|
private static CancellationTokenSource _tokenSource;
|
||||||
@@ -451,7 +453,7 @@ namespace Awake.Core
|
|||||||
Dictionary<string, uint> optionsList = new()
|
Dictionary<string, uint> optionsList = new()
|
||||||
{
|
{
|
||||||
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
|
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
|
||||||
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 },
|
{ string.Format(CultureInfo.InvariantCulture, AwakeHour, 1), 3600 },
|
||||||
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
|
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
|
||||||
};
|
};
|
||||||
return optionsList;
|
return optionsList;
|
||||||
|
|||||||
@@ -159,6 +159,15 @@ 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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to {0} hours.
|
/// Looks up a localized string similar to {0} hours.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -240,6 +249,15 @@ 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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to {0} minutes.
|
/// Looks up a localized string similar to {0} minutes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -123,6 +123,10 @@
|
|||||||
<data name="AWAKE_EXIT" xml:space="preserve">
|
<data name="AWAKE_EXIT" xml:space="preserve">
|
||||||
<value>Exit</value>
|
<value>Exit</value>
|
||||||
</data>
|
</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">
|
<data name="AWAKE_HOURS" xml:space="preserve">
|
||||||
<value>{0} hours</value>
|
<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>
|
<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>
|
||||||
@@ -142,6 +146,10 @@
|
|||||||
<value>Keep awake until expiration date and time</value>
|
<value>Keep awake until expiration date and time</value>
|
||||||
<comment>Keep the system awake until expiration date and time</comment>
|
<comment>Keep the system awake until expiration date and time</comment>
|
||||||
</data>
|
</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">
|
<data name="AWAKE_MINUTES" xml:space="preserve">
|
||||||
<value>{0} minutes</value>
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// 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.CodeAnalysis;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Common.Commands;
|
||||||
|
|
||||||
|
public sealed partial class ConfirmableCommand : InvokableCommand
|
||||||
|
{
|
||||||
|
private readonly IInvokableCommand? _command;
|
||||||
|
|
||||||
|
public Func<bool>? IsConfirmationRequired { get; init; }
|
||||||
|
|
||||||
|
public required string ConfirmationTitle { get; init; }
|
||||||
|
|
||||||
|
public required string ConfirmationMessage { get; init; }
|
||||||
|
|
||||||
|
public required IInvokableCommand Command
|
||||||
|
{
|
||||||
|
get => _command!;
|
||||||
|
init
|
||||||
|
{
|
||||||
|
if (_command is INotifyPropChanged oldNotifier)
|
||||||
|
{
|
||||||
|
oldNotifier.PropChanged -= InnerCommand_PropChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_command = value;
|
||||||
|
|
||||||
|
if (_command is INotifyPropChanged notifier)
|
||||||
|
{
|
||||||
|
notifier.PropChanged += InnerCommand_PropChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Name));
|
||||||
|
OnPropertyChanged(nameof(Id));
|
||||||
|
OnPropertyChanged(nameof(Icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Name
|
||||||
|
{
|
||||||
|
get => (_command as Command)?.Name ?? base.Name;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_command is Command cmd)
|
||||||
|
{
|
||||||
|
cmd.Name = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.Name = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Id
|
||||||
|
{
|
||||||
|
get => (_command as Command)?.Id ?? base.Id;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var previous = Id;
|
||||||
|
if (_command is Command cmd)
|
||||||
|
{
|
||||||
|
cmd.Id = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.Id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous != Id)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IconInfo Icon
|
||||||
|
{
|
||||||
|
get => (_command as Command)?.Icon ?? base.Icon;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_command is Command cmd)
|
||||||
|
{
|
||||||
|
cmd.Icon = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.Icon = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfirmableCommand()
|
||||||
|
{
|
||||||
|
// Allow init-only construction
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetsRequiredMembers]
|
||||||
|
public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func<bool>? isConfirmationRequired = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage);
|
||||||
|
ArgumentNullException.ThrowIfNull(confirmationMessage);
|
||||||
|
|
||||||
|
IsConfirmationRequired = isConfirmationRequired;
|
||||||
|
ConfirmationTitle = confirmationTitle;
|
||||||
|
ConfirmationMessage = confirmationMessage;
|
||||||
|
Command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args)
|
||||||
|
{
|
||||||
|
var property = args.PropertyName;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(property) || property == nameof(Name))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(property) || property == nameof(Id))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(property) || property == nameof(Icon))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ICommandResult Invoke()
|
||||||
|
{
|
||||||
|
var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true;
|
||||||
|
if (showConfirmationDialog)
|
||||||
|
{
|
||||||
|
return CommandResult.Confirm(new ConfirmationArgs
|
||||||
|
{
|
||||||
|
Title = ConfirmationTitle,
|
||||||
|
Description = ConfirmationMessage,
|
||||||
|
PrimaryCommand = Command,
|
||||||
|
IsPrimaryCommandCritical = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Command.Invoke(this) ?? CommandResult.Dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Common.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Well-known key chords used in the Command Palette and extensions.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Assigned key chords should not conflict with system or application shortcuts.
|
||||||
|
/// However, the key chords in this class are not guaranteed to be unique and may conflict
|
||||||
|
/// with each other, especially when commands appear together in the same menu.
|
||||||
|
/// </remarks>
|
||||||
|
public static class WellKnownKeyChords
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the well-known key chord for opening the file location. Shortcut: Ctrl+Shift+E.
|
||||||
|
/// </summary>
|
||||||
|
public static KeyChord OpenFileLocation { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.E);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the well-known key chord for copying the file path. Shortcut: Ctrl+Shift+C.
|
||||||
|
/// </summary>
|
||||||
|
public static KeyChord CopyFilePath { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.C);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the well-known key chord for opening the current location in a console. Shortcut: Ctrl+Shift+R.
|
||||||
|
/// </summary>
|
||||||
|
public static KeyChord OpenInConsole { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.R);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the well-known key chord for running the selected item as administrator. Shortcut: Ctrl+Shift+Enter.
|
||||||
|
/// </summary>
|
||||||
|
public static KeyChord RunAsAdministrator { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.Enter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the well-known key chord for running the selected item as a different user. Shortcut: Ctrl+Shift+U.
|
||||||
|
/// </summary>
|
||||||
|
public static KeyChord RunAsDifferentUser { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.U);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the well-known key chord for toggling the pin state. Shortcut: Ctrl+P.
|
||||||
|
/// </summary>
|
||||||
|
public static KeyChord TogglePin { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: (int)VirtualKey.P);
|
||||||
|
}
|
||||||
@@ -160,7 +160,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
Initialized |= InitializedState.Initialized;
|
Initialized |= InitializedState.Initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SlowInitializeProperties()
|
public virtual void SlowInitializeProperties()
|
||||||
{
|
{
|
||||||
if (IsSelectedInitialized)
|
if (IsSelectedInitialized)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,9 +47,21 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
|
|
||||||
UpdateTags(li.Tags);
|
UpdateTags(li.Tags);
|
||||||
|
|
||||||
TextToSuggest = li.TextToSuggest;
|
|
||||||
Section = li.Section ?? string.Empty;
|
Section = li.Section ?? string.Empty;
|
||||||
var extensionDetails = li.Details;
|
|
||||||
|
UpdateProperty(nameof(Section));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SlowInitializeProperties()
|
||||||
|
{
|
||||||
|
base.SlowInitializeProperties();
|
||||||
|
var model = Model.Unsafe;
|
||||||
|
if (model is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extensionDetails = model.Details;
|
||||||
if (extensionDetails is not null)
|
if (extensionDetails is not null)
|
||||||
{
|
{
|
||||||
Details = new(extensionDetails, PageContext);
|
Details = new(extensionDetails, PageContext);
|
||||||
@@ -58,8 +70,8 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
UpdateProperty(nameof(HasDetails));
|
UpdateProperty(nameof(HasDetails));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextToSuggest = model.TextToSuggest;
|
||||||
UpdateProperty(nameof(TextToSuggest));
|
UpdateProperty(nameof(TextToSuggest));
|
||||||
UpdateProperty(nameof(Section));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void FetchProperty(string propertyName)
|
protected override void FetchProperty(string propertyName)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using ManagedCommon;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ public partial class AppStateModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(FilePath))
|
if (string.IsNullOrEmpty(FilePath))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadState)}");
|
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(FilePath))
|
if (!File.Exists(FilePath))
|
||||||
@@ -77,43 +79,84 @@ public partial class AppStateModel : ObservableObject
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Serialize the main dictionary to JSON and save it to the file
|
// 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!);
|
||||||
|
|
||||||
// Is it valid JSON?
|
// validate JSON
|
||||||
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
|
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
|
||||||
{
|
{
|
||||||
// Now, read the existing content from the file
|
Logger.LogError("Failed to parse app state as a JsonObject.");
|
||||||
var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}";
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Is it valid JSON?
|
// read previous settings
|
||||||
if (JsonNode.Parse(oldContent) is JsonObject savedSettings)
|
if (!TryReadSavedState(out var savedSettings))
|
||||||
{
|
{
|
||||||
foreach (var item in newSettings)
|
savedSettings = new JsonObject();
|
||||||
{
|
}
|
||||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
|
||||||
}
|
|
||||||
|
|
||||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
|
// merge new settings into old ones
|
||||||
File.WriteAllText(FilePath, serialized);
|
foreach (var item in newSettings)
|
||||||
|
{
|
||||||
|
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Instead of just raising the event here, we should
|
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
|
||||||
// have a file change watcher on the settings file, and
|
File.WriteAllText(FilePath, serialized);
|
||||||
// reload the settings then
|
|
||||||
model.StateChanged?.Invoke(model, null);
|
// TODO: Instead of just raising the event here, we should
|
||||||
}
|
// have a file change watcher on the settings file, and
|
||||||
else
|
// reload the settings then
|
||||||
{
|
model.StateChanged?.Invoke(model, null);
|
||||||
Debug.WriteLine("Failed to parse settings file as JsonObject.");
|
}
|
||||||
}
|
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);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug.WriteLine("Failed to parse settings file as JsonObject.");
|
// file doesn't exist (might not have been created yet), so consider this a success
|
||||||
|
// and return empty settings
|
||||||
|
savedSettings = new JsonObject();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.WriteLine(ex.ToString());
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -19,6 +20,10 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem
|
|||||||
Title = string.Empty;
|
Title = string.Empty;
|
||||||
_logMessagesPage.Name = string.Empty;
|
_logMessagesPage.Name = string.Empty;
|
||||||
Subtitle = Properties.Resources.builtin_log_subtitle;
|
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)
|
public override void UpdateQuery(string query)
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
private readonly TopLevelCommandManager _tlcManager;
|
private readonly TopLevelCommandManager _tlcManager;
|
||||||
private IEnumerable<IListItem>? _filteredItems;
|
private IEnumerable<Scored<IListItem>>? _filteredItems;
|
||||||
|
private IEnumerable<Scored<IListItem>>? _filteredApps;
|
||||||
|
private IEnumerable<IListItem>? _allApps;
|
||||||
private bool _includeApps;
|
private bool _includeApps;
|
||||||
private bool _filteredItemsIncludesApps;
|
private bool _filteredItemsIncludesApps;
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
|
RaiseItemsChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +150,13 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
{
|
{
|
||||||
lock (_tlcManager.TopLevelCommands)
|
lock (_tlcManager.TopLevelCommands)
|
||||||
{
|
{
|
||||||
return _filteredItems?.ToArray() ?? [];
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +175,8 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
{
|
{
|
||||||
_filteredItemsIncludesApps = _includeApps;
|
_filteredItemsIncludesApps = _includeApps;
|
||||||
_filteredItems = null;
|
_filteredItems = null;
|
||||||
|
_filteredApps = null;
|
||||||
|
_allApps = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +194,8 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
{
|
{
|
||||||
_filteredItemsIncludesApps = _includeApps;
|
_filteredItemsIncludesApps = _includeApps;
|
||||||
_filteredItems = null;
|
_filteredItems = null;
|
||||||
|
_filteredApps = null;
|
||||||
|
_allApps = null;
|
||||||
RaiseItemsChanged(commands.Count);
|
RaiseItemsChanged(commands.Count);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,35 +205,49 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
_filteredItems = null;
|
_filteredItems = null;
|
||||||
|
_filteredApps = null;
|
||||||
|
_allApps = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the internal state has changed, reset _filteredItems to reset the list.
|
// If the internal state has changed, reset _filteredItems to reset the list.
|
||||||
if (_filteredItemsIncludesApps != _includeApps)
|
if (_filteredItemsIncludesApps != _includeApps)
|
||||||
{
|
{
|
||||||
_filteredItems = null;
|
_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
|
// If we don't have any previous filter results to work with, start
|
||||||
// with a list of all our commands & apps.
|
// with a list of all our commands & apps.
|
||||||
if (_filteredItems is null)
|
if (newFilteredItems is null && _filteredApps is null)
|
||||||
{
|
{
|
||||||
_filteredItems = commands;
|
newFilteredItems = commands;
|
||||||
_filteredItemsIncludesApps = _includeApps;
|
_filteredItemsIncludesApps = _includeApps;
|
||||||
|
|
||||||
if (_includeApps)
|
if (_includeApps)
|
||||||
{
|
{
|
||||||
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
|
_allApps = 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.
|
// Produce a list of everything that matches the current filter.
|
||||||
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
|
_filteredItems = ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem);
|
||||||
RaiseItemsChanged(_filteredItems.Count());
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ public class ExtensionWrapper : IExtensionWrapper
|
|||||||
// We'll just return out nothing.
|
// We'll just return out nothing.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (hr.Value != 0)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
// Marshal.ThrowExceptionForHR(hr);
|
// Marshal.ThrowExceptionForHR(hr);
|
||||||
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
|
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
|
||||||
|
|||||||
@@ -285,6 +285,15 @@ 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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to View log.
|
/// Looks up a localized string similar to View log.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -135,6 +135,9 @@
|
|||||||
<data name="builtin_log_title" xml:space="preserve">
|
<data name="builtin_log_title" xml:space="preserve">
|
||||||
<value>View log</value>
|
<value>View log</value>
|
||||||
</data>
|
</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">
|
<data name="builtin_reload_subtitle" xml:space="preserve">
|
||||||
<value>Reload Command Palette extensions</value>
|
<value>Reload Command Palette extensions</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
private string _generatedId = string.Empty;
|
private string _generatedId = string.Empty;
|
||||||
|
|
||||||
private HotkeySettings? _hotkey;
|
private HotkeySettings? _hotkey;
|
||||||
|
private IIconInfo? _initialIcon;
|
||||||
|
|
||||||
private CommandAlias? Alias { get; set; }
|
private CommandAlias? Alias { get; set; }
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
|
|
||||||
public IIconInfo Icon => _commandItemViewModel.Icon;
|
public IIconInfo Icon => _commandItemViewModel.Icon;
|
||||||
|
|
||||||
|
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
|
||||||
|
|
||||||
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
||||||
|
|
||||||
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
|
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
|
||||||
@@ -205,6 +208,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
{
|
{
|
||||||
DisplayTitle = fallback.DisplayTitle;
|
DisplayTitle = fallback.DisplayTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateInitialIcon(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +226,31 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
FetchAliasFromAliasManager();
|
FetchAliasFromAliasManager();
|
||||||
UpdateHotkey();
|
UpdateHotkey();
|
||||||
UpdateTags();
|
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)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public partial class App : Application
|
|||||||
AppWindow = new MainWindow();
|
AppWindow = new MainWindow();
|
||||||
|
|
||||||
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
|
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs);
|
((MainWindow)AppWindow).HandleLaunchNonUI(activatedEventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets focus to the "More" button after closing the context menu,
|
||||||
|
/// keeping keyboard navigation intuitive.
|
||||||
|
/// </summary>
|
||||||
|
public void FocusMoreCommandsButton()
|
||||||
|
{
|
||||||
|
MoreCommandsButton?.Focus(FocusState.Programmatic);
|
||||||
|
}
|
||||||
|
|
||||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||||
{
|
{
|
||||||
// We need to wait until our flyout is opened to try and toss focus
|
// We need to wait until our flyout is opened to try and toss focus
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
|
PreviewKeyDown="UserControl_PreviewKeyDown"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.Messages;
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
@@ -115,6 +116,24 @@ public sealed partial class ContextMenu : UserControl,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles Escape to close the context menu and return focus to the "More" button.
|
||||||
|
/// </summary>
|
||||||
|
private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == VirtualKey.Escape)
|
||||||
|
{
|
||||||
|
// Close the context menu (if not already handled)
|
||||||
|
WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage());
|
||||||
|
|
||||||
|
// Find the parent CommandBar and set focus to MoreCommandsButton
|
||||||
|
var parent = this.FindParent<CommandBar>();
|
||||||
|
parent?.FocusMoreCommandsButton();
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var prop = e.PropertyName;
|
var prop = e.PropertyName;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.Deferred;
|
using Microsoft.CmdPal.UI.Deferred;
|
||||||
|
using Microsoft.Terminal.UI;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@@ -55,6 +57,8 @@ public partial class IconBox : ContentControl
|
|||||||
{
|
{
|
||||||
TabFocusNavigation = KeyboardNavigationMode.Once;
|
TabFocusNavigation = KeyboardNavigationMode.Once;
|
||||||
IsTabStop = false;
|
IsTabStop = false;
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Center;
|
||||||
|
VerticalContentAlignment = VerticalAlignment.Center;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
@@ -75,6 +79,8 @@ public partial class IconBox : ContentControl
|
|||||||
IconSourceElement elem = new()
|
IconSourceElement elem = new()
|
||||||
{
|
{
|
||||||
IconSource = fontIco,
|
IconSource = fontIco,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
};
|
};
|
||||||
@this.Content = elem;
|
@this.Content = elem;
|
||||||
break;
|
break;
|
||||||
@@ -98,14 +104,20 @@ public partial class IconBox : ContentControl
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO GH #239 switch back when using the new MD text block
|
// TODO GH #239 switch back when using the new MD text block
|
||||||
|
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
|
||||||
// _ = @this._queue.EnqueueAsync(() =>
|
// _ = @this._queue.EnqueueAsync(() =>
|
||||||
@this._queue.TryEnqueue(new(async () =>
|
@this._queue.TryEnqueue(async void () =>
|
||||||
{
|
{
|
||||||
var requestedTheme = @this.ActualTheme;
|
try
|
||||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
|
||||||
|
|
||||||
if (@this.SourceRequested is not null)
|
|
||||||
{
|
{
|
||||||
|
if (@this.SourceRequested is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestedTheme = @this.ActualTheme;
|
||||||
|
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||||
|
|
||||||
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
||||||
|
|
||||||
// After the await:
|
// After the await:
|
||||||
@@ -130,37 +142,35 @@ public partial class IconBox : ContentControl
|
|||||||
// So, if the icon we get back was a font icon,
|
// So, if the icon we get back was a font icon,
|
||||||
// and the glyph for that icon is NOT in the range of
|
// and the glyph for that icon is NOT in the range of
|
||||||
// Segoe icons, then let's give the icon some extra space
|
// Segoe icons, then let's give the icon some extra space
|
||||||
@this.Padding = new Thickness(0);
|
var iconData = eventArgs.Key switch
|
||||||
|
|
||||||
IconDataViewModel? iconData = null;
|
|
||||||
if (eventArgs.Key is IconDataViewModel)
|
|
||||||
{
|
{
|
||||||
iconData = eventArgs.Key as IconDataViewModel;
|
IconDataViewModel key => key,
|
||||||
|
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (iconData?.Icon is not null && @this.Source is FontIconSource)
|
||||||
|
{
|
||||||
|
var iconSize =
|
||||||
|
!double.IsNaN(@this.Width) ? @this.Width :
|
||||||
|
!double.IsNaN(@this.Height) ? @this.Height :
|
||||||
|
@this.ActualWidth > 0 ? @this.ActualWidth :
|
||||||
|
@this.ActualHeight;
|
||||||
|
|
||||||
|
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
|
||||||
}
|
}
|
||||||
else if (eventArgs.Key is IconInfoViewModel info)
|
else
|
||||||
{
|
{
|
||||||
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
|
@this.Padding = default;
|
||||||
}
|
|
||||||
|
|
||||||
if (iconData is not null &&
|
|
||||||
@this.Source is FontIconSource)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
|
|
||||||
{
|
|
||||||
var ch = iconData.Icon[0];
|
|
||||||
|
|
||||||
// The range of MDL2 Icons isn't explicitly defined, but
|
|
||||||
// we're using this based off the table on:
|
|
||||||
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
|
|
||||||
var isMDL2Icon = ch is >= '\uE700' and <= '\uF8FF';
|
|
||||||
if (!isMDL2Icon)
|
|
||||||
{
|
|
||||||
@this.Padding = new Thickness(-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Exception from TryEnqueue bypasses the global error handler,
|
||||||
|
// and crashes the app.
|
||||||
|
Logger.LogError("Failed to set icon", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
_localKeyboardListener = new LocalKeyboardListener();
|
_localKeyboardListener = new LocalKeyboardListener();
|
||||||
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
|
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
|
||||||
_localKeyboardListener.Start();
|
_localKeyboardListener.Start();
|
||||||
|
|
||||||
|
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
|
||||||
|
HideWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||||
@@ -233,9 +236,6 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
{
|
{
|
||||||
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
|
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
|
||||||
|
|
||||||
// Make sure our HWND is cloaked before any possible window manipulations
|
|
||||||
Cloak();
|
|
||||||
|
|
||||||
// Remember, IsIconic == "minimized", which is entirely different state
|
// Remember, IsIconic == "minimized", which is entirely different state
|
||||||
// from "show/hide"
|
// from "show/hide"
|
||||||
// If we're currently minimized, restore us first, before we reveal
|
// If we're currently minimized, restore us first, before we reveal
|
||||||
@@ -243,6 +243,9 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// which would remain not visible to the user.
|
// which would remain not visible to the user.
|
||||||
if (PInvoke.IsIconic(hwnd))
|
if (PInvoke.IsIconic(hwnd))
|
||||||
{
|
{
|
||||||
|
// Make sure our HWND is cloaked before any possible window manipulations
|
||||||
|
Cloak();
|
||||||
|
|
||||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,8 +484,13 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
|
public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
|
||||||
{
|
{
|
||||||
|
// LOAD BEARING
|
||||||
|
// Any reading and processing of the activation arguments must be done
|
||||||
|
// synchronously in this method, before it returns. The sending instance
|
||||||
|
// remains blocked until this returns; afterward it may quit, causing
|
||||||
|
// the activation arguments to be lost.
|
||||||
if (activatedEventArgs is null)
|
if (activatedEventArgs is null)
|
||||||
{
|
{
|
||||||
Summon(string.Empty);
|
Summon(string.Empty);
|
||||||
@@ -519,9 +527,26 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
}
|
}
|
||||||
catch (COMException ex)
|
catch (COMException ex)
|
||||||
{
|
{
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
|
||||||
|
const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
|
||||||
|
const int RPC_S_CALL_FAILED = 2147023170;
|
||||||
|
|
||||||
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
|
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
|
||||||
// if the args are not valid or not passed correctly.
|
// if the args are not valid or not passed correctly.
|
||||||
Logger.LogError("COM exception when activating the application", ex);
|
if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(
|
||||||
|
$"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
|
||||||
|
$"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
|
||||||
|
$"The application will continue running and fall back to showing the Command Palette window.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError(
|
||||||
|
$"COM exception (HRESULT {ex.HResult}) when activating the application. " +
|
||||||
|
$"The application will continue running and fall back to showing the Command Palette window.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Summon(string.Empty);
|
Summon(string.Empty);
|
||||||
@@ -610,6 +635,20 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void HandleSummon(string commandId)
|
private void HandleSummon(string commandId)
|
||||||
|
{
|
||||||
|
if (_ignoreHotKeyWhenFullScreen)
|
||||||
|
{
|
||||||
|
// If we're in full screen mode, ignore the hotkey
|
||||||
|
if (WindowHelper.IsWindowFullscreen())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleSummonCore(commandId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSummonCore(string commandId)
|
||||||
{
|
{
|
||||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
||||||
@@ -634,8 +673,6 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// so that we can bind hotkeys to individual commands
|
// so that we can bind hotkeys to individual commands
|
||||||
if (!isVisible || !isRootHotkey)
|
if (!isVisible || !isRootHotkey)
|
||||||
{
|
{
|
||||||
Activate();
|
|
||||||
|
|
||||||
Summon(commandId);
|
Summon(commandId);
|
||||||
}
|
}
|
||||||
else if (isRootHotkey)
|
else if (isRootHotkey)
|
||||||
@@ -671,15 +708,6 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
var hotkeyIndex = (int)wParam.Value;
|
var hotkeyIndex = (int)wParam.Value;
|
||||||
if (hotkeyIndex < _hotkeys.Count)
|
if (hotkeyIndex < _hotkeys.Count)
|
||||||
{
|
{
|
||||||
if (_ignoreHotKeyWhenFullScreen)
|
|
||||||
{
|
|
||||||
// If we're in full screen mode, ignore the hotkey
|
|
||||||
if (WindowHelper.IsWindowFullscreen())
|
|
||||||
{
|
|
||||||
return (LRESULT)IntPtr.Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hotkey = _hotkeys[hotkeyIndex];
|
var hotkey = _hotkeys[hotkeyIndex];
|
||||||
HandleSummon(hotkey.CommandId);
|
HandleSummon(hotkey.CommandId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,12 +107,33 @@ internal sealed class Program
|
|||||||
{
|
{
|
||||||
// Do the redirection on another thread, and use a non-blocking
|
// Do the redirection on another thread, and use a non-blocking
|
||||||
// wait method to wait for the redirection to complete.
|
// wait method to wait for the redirection to complete.
|
||||||
var redirectSemaphore = new Semaphore(0, 1);
|
using var redirectSemaphore = new Semaphore(0, 1);
|
||||||
Task.Run(() =>
|
var redirectTimeout = TimeSpan.FromSeconds(32);
|
||||||
|
|
||||||
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
using var cts = new CancellationTokenSource(redirectTimeout);
|
||||||
redirectSemaphore.Release();
|
try
|
||||||
|
{
|
||||||
|
keyInstance.RedirectActivationToAsync(args)
|
||||||
|
.AsTask(cts.Token)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to activate existing instance; timed out after {redirectTimeout}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Failed to activate existing instance", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
redirectSemaphore.Release();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = PInvoke.CoWaitForMultipleObjects(
|
_ = PInvoke.CoWaitForMultipleObjects(
|
||||||
(uint)CWMO_FLAGS.CWMO_DEFAULT,
|
(uint)CWMO_FLAGS.CWMO_DEFAULT,
|
||||||
PInvoke.INFINITE,
|
PInvoke.INFINITE,
|
||||||
@@ -124,13 +145,14 @@ internal sealed class Program
|
|||||||
{
|
{
|
||||||
// If we already have a form, display the message now.
|
// If we already have a form, display the message now.
|
||||||
// Otherwise, add it to the collection for displaying later.
|
// Otherwise, add it to the collection for displaying later.
|
||||||
if (App.Current is App thisApp)
|
if (App.Current?.AppWindow is MainWindow mainWindow)
|
||||||
{
|
{
|
||||||
if (thisApp.AppWindow is not null and
|
// LOAD BEARING
|
||||||
MainWindow mainWindow)
|
// This must be synchronous to ensure the method does not return
|
||||||
{
|
// before the activation is fully handled and the parameters are processed.
|
||||||
uiContext?.Post(_ => mainWindow.HandleLaunch(args), null);
|
// The sending instance remains blocked until this returns; afterward it may quit,
|
||||||
}
|
// causing the activation arguments to be lost.
|
||||||
|
mainWindow.HandleLaunchNonUI(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
Width="20"
|
Width="20"
|
||||||
Height="20"
|
Height="20"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind InitialIcon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||||
</cpcontrols:ContentIcon.Content>
|
</cpcontrols:ContentIcon.Content>
|
||||||
</cpcontrols:ContentIcon>
|
</cpcontrols:ContentIcon>
|
||||||
|
|||||||
@@ -24,23 +24,15 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<!-- TO DO: Replace this with WinUI TitleBar once that ships. -->
|
<TitleBar x:Name="TitleBar">
|
||||||
<StackPanel
|
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||||
x:Name="AppTitleBar"
|
<TitleBar.LeftHeader>
|
||||||
Grid.Row="0"
|
<ImageIcon
|
||||||
Height="48"
|
Height="16"
|
||||||
Margin="16,0,0,0"
|
Margin="16,0,0,0"
|
||||||
Orientation="Horizontal">
|
Source="ms-appx:///Assets/icon.svg" />
|
||||||
<Image
|
</TitleBar.LeftHeader>
|
||||||
Width="16"
|
</TitleBar>
|
||||||
Height="16"
|
|
||||||
Source="ms-appx:///Assets/icon.svg" />
|
|
||||||
<TextBlock
|
|
||||||
x:Uid="CmdPalSettingsHeader"
|
|
||||||
Margin="12,0,0,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Style="{StaticResource CaptionTextBlockStyle}" />
|
|
||||||
</StackPanel>
|
|
||||||
<NavigationView
|
<NavigationView
|
||||||
x:Name="NavView"
|
x:Name="NavView"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@@ -77,7 +69,6 @@
|
|||||||
x:Name="NavigationBreadcrumbBar"
|
x:Name="NavigationBreadcrumbBar"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
MaxWidth="1000"
|
MaxWidth="1000"
|
||||||
Margin="16,0,0,0"
|
|
||||||
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
|
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
|
||||||
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
|
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
|
||||||
<BreadcrumbBar.ItemTemplate>
|
<BreadcrumbBar.ItemTemplate>
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ public sealed partial class SettingsWindow : WindowEx,
|
|||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
this.ExtendsContentIntoTitleBar = true;
|
this.ExtendsContentIntoTitleBar = true;
|
||||||
this.SetIcon();
|
this.SetIcon();
|
||||||
this.AppWindow.Title = RS_.GetString("SettingsWindowTitle");
|
var title = RS_.GetString("SettingsWindowTitle");
|
||||||
|
this.AppWindow.Title = title;
|
||||||
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
|
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
|
||||||
|
this.TitleBar.Title = title;
|
||||||
PositionCentered();
|
PositionCentered();
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
|
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<data name="NavigationPaneClosed" xml:space="preserve">
|
<data name="NavigationPaneClosed" xml:space="preserve">
|
||||||
<value>Navigation pane closed</value>
|
<value>Navigation pane closed</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NavigationPageOpened" xml:space="preserve">
|
<data name="NavigationPaneOpened" xml:space="preserve">
|
||||||
<value>Navigation page opened</value>
|
<value>Navigation page opened</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
#include "FontIconGlyphClassifier.h"
|
||||||
|
#include "FontIconGlyphClassifier.g.cpp"
|
||||||
|
|
||||||
|
#include <icu.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
|
||||||
|
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
|
||||||
|
{
|
||||||
|
static constexpr UChar32 _fluentIconsPrivateUseAreaStart = 0xE700;
|
||||||
|
static constexpr UChar32 _fluentIconsPrivateUseAreaEnd = 0xF8FF;
|
||||||
|
return cp >= _fluentIconsPrivateUseAreaStart && cp <= _fluentIconsPrivateUseAreaEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if the given text (as a sequence of UChar code units) is emoji
|
||||||
|
[[nodiscard]] bool _isEmoji(const UChar* p, const int32_t length) noexcept
|
||||||
|
{
|
||||||
|
if (!p || length < 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.unicode.org/reports/tr51/#Emoji_Variation_Selector_Notes
|
||||||
|
constexpr UChar32 vs15CodePoint = 0xFE0E; // Variation Selectors 15: text variation selector
|
||||||
|
constexpr UChar32 vs16CodePoint = 0xFE0F; // Variation Selectors: 16 emoji variation selector
|
||||||
|
|
||||||
|
// Decode the first code point correctly (surrogate-safe)
|
||||||
|
int32_t i0{ 0 };
|
||||||
|
UChar32 first{ 0 };
|
||||||
|
U16_NEXT(p, i0, length, first);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < length;)
|
||||||
|
{
|
||||||
|
UChar32 cp{ 0 };
|
||||||
|
U16_NEXT(p, i, length, cp);
|
||||||
|
|
||||||
|
if (cp == vs16CodePoint) { return true; }
|
||||||
|
if (cp == vs15CodePoint) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return !U_IS_SURROGATE(first) && u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontIconGlyphClassifier::IsLikelyToBeEmojiOrSymbolIcon(const hstring& text)
|
||||||
|
{
|
||||||
|
if (text.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.size() == 1 && !IS_HIGH_SURROGATE(text[0]))
|
||||||
|
{
|
||||||
|
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
|
||||||
|
// If it turns out to be illegal Unicode, we don't really care.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.size() >= 2 && text[0] <= 0x7F && text[1] <= 0x7F)
|
||||||
|
{
|
||||||
|
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
|
||||||
|
// grapheme cluster.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ICU to determine whether text is composed of a single grapheme cluster.
|
||||||
|
int32_t off{ 0 };
|
||||||
|
UErrorCode status{ U_ZERO_ERROR };
|
||||||
|
|
||||||
|
UBreakIterator* const bi{ ubrk_open(UBRK_CHARACTER,
|
||||||
|
nullptr,
|
||||||
|
reinterpret_cast<const UChar*>(text.data()),
|
||||||
|
static_cast<int>(text.size()),
|
||||||
|
&status) };
|
||||||
|
if (bi)
|
||||||
|
{
|
||||||
|
if (U_SUCCESS(status))
|
||||||
|
{
|
||||||
|
off = ubrk_next(bi);
|
||||||
|
}
|
||||||
|
ubrk_close(bi);
|
||||||
|
}
|
||||||
|
return std::cmp_equal(off, text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
FontIconGlyphKind FontIconGlyphClassifier::Classify(hstring const& text) noexcept
|
||||||
|
{
|
||||||
|
if (text.empty())
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t textSize{ text.size() };
|
||||||
|
const auto* buffer{ reinterpret_cast<const UChar*>(text.c_str()) };
|
||||||
|
|
||||||
|
// Fast path 1: Single UTF-16 code unit (most common case)
|
||||||
|
if (textSize == 1)
|
||||||
|
{
|
||||||
|
const UChar ch{ buffer[0] };
|
||||||
|
|
||||||
|
if (IS_HIGH_SURROGATE(ch))
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isFluentIconPua(ch))
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::FluentSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isEmoji(&ch, 1))
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FontIconGlyphKind::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path 2: Common file path pattern - two ASCII printable characters
|
||||||
|
if (textSize >= 2 && buffer[0] <= 0x7F && buffer[1] <= 0x7F)
|
||||||
|
{
|
||||||
|
// Definitely multiple graphemes
|
||||||
|
return FontIconGlyphKind::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expensive path: Use ICU to determine grapheme boundaries
|
||||||
|
UErrorCode status{ U_ZERO_ERROR };
|
||||||
|
|
||||||
|
UBreakIterator* bi{ ubrk_open(UBRK_CHARACTER,
|
||||||
|
nullptr,
|
||||||
|
buffer,
|
||||||
|
static_cast<int32_t>(textSize),
|
||||||
|
&status) };
|
||||||
|
|
||||||
|
if (U_FAILURE(status) || !bi)
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t start{ ubrk_first(bi) };
|
||||||
|
const int32_t end{ ubrk_next(bi) }; // end of first grapheme
|
||||||
|
ubrk_close(bi);
|
||||||
|
|
||||||
|
// No graphemes found
|
||||||
|
if (end == UBRK_DONE || end <= start)
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's more than one grapheme, it's not a valid icon glyph
|
||||||
|
if (std::cmp_not_equal(end, textSize))
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly one grapheme: classify
|
||||||
|
const UChar* grapheme = buffer + start;
|
||||||
|
const int32_t graphemeLength = end - start;
|
||||||
|
|
||||||
|
if (graphemeLength == 1 && _isFluentIconPua(grapheme[0]))
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::FluentSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isEmoji(grapheme, graphemeLength))
|
||||||
|
{
|
||||||
|
return FontIconGlyphKind::Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FontIconGlyphKind::Other;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "FontIconGlyphClassifier.g.h"
|
||||||
|
|
||||||
|
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||||
|
{
|
||||||
|
struct FontIconGlyphClassifier
|
||||||
|
{
|
||||||
|
[[nodiscard]] static bool IsLikelyToBeEmojiOrSymbolIcon(const winrt::hstring& text);
|
||||||
|
|
||||||
|
[[nodiscard]] static FontIconGlyphKind Classify(winrt::hstring const& text) noexcept;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace winrt::Microsoft::Terminal::UI::factory_implementation
|
||||||
|
{
|
||||||
|
BASIC_FACTORY(FontIconGlyphClassifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.Terminal.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Categorizes the type of a single grapheme cluster or input text.
|
||||||
|
/// Used to determine how the input should be handled or rendered (for example,
|
||||||
|
/// whether it should be treated as an emoji, an icon from a symbol font, plain text, etc.).
|
||||||
|
/// </summary>
|
||||||
|
enum FontIconGlyphKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Input is invalid or contains more than one grapheme cluster and therefore cannot be
|
||||||
|
/// treated as a single symbol. Typical for multi-character text like file paths
|
||||||
|
/// or composed strings that include separators.
|
||||||
|
/// </summary>
|
||||||
|
Invalid = -1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No grapheme present (empty string). Indicates absence of a symbol.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single emoji grapheme cluster. This may consist of multiple Unicode code
|
||||||
|
/// points combined into one visible glyph (e.g., emoji with modifiers or ZWJ sequences).
|
||||||
|
/// </summary>
|
||||||
|
Emoji = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single glyph from the Segoe Fluent Icons / MDL2 Assets Private Use Area (PUA),
|
||||||
|
/// typically in the Unicode range U+E700–U+F8FF. These are font-based icons (Fluent/MDL2).
|
||||||
|
/// </summary>
|
||||||
|
FluentSymbol = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single non-emoji grapheme that is not a Fluent/MDL2 PUA symbol.
|
||||||
|
/// Covers ordinary characters, letters, numbers, or other single glyph symbols.
|
||||||
|
/// </summary>
|
||||||
|
Other = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static utility class for text and icon analysis
|
||||||
|
/// </summary>
|
||||||
|
static runtimeclass FontIconGlyphClassifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if text represents a single grapheme cluster (emoji/symbol icon).
|
||||||
|
/// Uses ICU for Unicode boundary detection to distinguish icons from file paths.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">Text to analyze</param>
|
||||||
|
/// <returns>True if single grapheme cluster, false for multi-character text or paths</returns>
|
||||||
|
static Boolean IsLikelyToBeEmojiOrSymbolIcon(String text);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Classifies the input into a glyph kind suitable for icon or text rendering.
|
||||||
|
/// </summary>
|
||||||
|
static FontIconGlyphKind Classify(String text);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "IconPathConverter.h"
|
#include "IconPathConverter.h"
|
||||||
#include "IconPathConverter.g.cpp"
|
#include "IconPathConverter.g.cpp"
|
||||||
|
|
||||||
// #include "Utils.h"
|
#include "FontIconGlyphClassifier.h"
|
||||||
|
|
||||||
#include <Shlobj.h>
|
#include <Shlobj.h>
|
||||||
#include <Shlobj_core.h>
|
#include <Shlobj_core.h>
|
||||||
@@ -110,7 +110,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
||||||
{
|
{
|
||||||
typename ImageIconSource<TIconSource>::type iconSource;
|
typename ImageIconSource<TIconSource>::type iconSource;
|
||||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||||
iconSource.ImageSource(source);
|
iconSource.ImageSource(source);
|
||||||
return iconSource;
|
return iconSource;
|
||||||
}
|
}
|
||||||
@@ -169,41 +169,46 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
|
|
||||||
// If we fail to set the icon source using the "icon" as a path,
|
// If we fail to set the icon source using the "icon" as a path,
|
||||||
// let's try it as a symbol/emoji.
|
// let's try it as a symbol/emoji.
|
||||||
//
|
if (!iconSource)
|
||||||
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so
|
|
||||||
// don't do this if it's just an invalid path.
|
|
||||||
if (!iconSource && iconPath.size() <= 2)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
typename FontIconSource<TIconSource>::type icon;
|
const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath);
|
||||||
const auto ch = til::at(iconPath, 0);
|
|
||||||
|
|
||||||
// The range of MDL2 Icons isn't explicitly defined, but
|
winrt::hstring family;
|
||||||
// we're using this based off the table on:
|
if (glyph_kind == FontIconGlyphKind::Invalid)
|
||||||
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
|
|
||||||
const auto isMDL2Icon = ch >= L'\uE700' && ch <= L'\uF8FF';
|
|
||||||
if (isMDL2Icon)
|
|
||||||
{
|
{
|
||||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
|
family = L"Segoe UI";
|
||||||
}
|
}
|
||||||
else if (!fontFamily.empty())
|
else if (!fontFamily.empty())
|
||||||
{
|
{
|
||||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily });
|
family = fontFamily;
|
||||||
|
}
|
||||||
|
else if (glyph_kind == FontIconGlyphKind::FluentSymbol)
|
||||||
|
{
|
||||||
|
family = L"Segoe Fluent Icons, Segoe MDL2 Assets";
|
||||||
|
}
|
||||||
|
else if (glyph_kind == FontIconGlyphKind::Emoji)
|
||||||
|
{
|
||||||
|
// Emoji and other symbols go in the Segoe UI Emoji font.
|
||||||
|
// Some emojis (e.g. 2️⃣) would be rendered as emoji glyphs otherwise.
|
||||||
|
family = L"Segoe UI Emoji, Segoe UI";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Note: you _do_ need to manually set the font here.
|
family = L"Segoe UI";
|
||||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe UI" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typename FontIconSource<TIconSource>::type icon;
|
||||||
|
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
|
||||||
icon.FontSize(targetSize);
|
icon.FontSize(targetSize);
|
||||||
icon.Glyph(iconPath);
|
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
|
||||||
iconSource = icon;
|
iconSource = icon;
|
||||||
}
|
}
|
||||||
CATCH_LOG();
|
CATCH_LOG();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iconSource)
|
if (!iconSource)
|
||||||
{
|
{
|
||||||
// Set the default IconSource to a BitmapIconSource with a null source
|
// Set the default IconSource to a BitmapIconSource with a null source
|
||||||
@@ -326,7 +331,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
}
|
}
|
||||||
|
|
||||||
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
|
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
|
||||||
int index,
|
int index,
|
||||||
int targetSize)
|
int targetSize)
|
||||||
{
|
{
|
||||||
// Try:
|
// Try:
|
||||||
|
|||||||
@@ -159,6 +159,9 @@
|
|||||||
<ClInclude Include="ResourceString.h">
|
<ClInclude Include="ResourceString.h">
|
||||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="FontIconGlyphClassifier.h">
|
||||||
|
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||||
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="init.cpp" />
|
<ClCompile Include="init.cpp" />
|
||||||
@@ -178,6 +181,9 @@
|
|||||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||||
|
<ClCompile Include="FontIconGlyphClassifier.cpp">
|
||||||
|
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Midl Include="Converters.idl" />
|
<Midl Include="Converters.idl" />
|
||||||
@@ -185,6 +191,7 @@
|
|||||||
<Midl Include="RunHistory.idl" />
|
<Midl Include="RunHistory.idl" />
|
||||||
<Midl Include="IDirectKeyListener.idl" />
|
<Midl Include="IDirectKeyListener.idl" />
|
||||||
<Midl Include="ResourceString.idl" />
|
<Midl Include="ResourceString.idl" />
|
||||||
|
<Midl Include="FontIconGlyphClassifier.idl" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||||
|
|
||||||
@@ -13,34 +12,22 @@ public class MockSettingsInterface : ISettingsInterface
|
|||||||
{
|
{
|
||||||
private readonly List<HistoryItem> _historyItems;
|
private readonly List<HistoryItem> _historyItems;
|
||||||
|
|
||||||
|
public event EventHandler HistoryChanged;
|
||||||
|
|
||||||
public bool GlobalIfURI { get; set; }
|
public bool GlobalIfURI { get; set; }
|
||||||
|
|
||||||
public string ShowHistory { get; set; }
|
public int HistoryItemCount { get; set; }
|
||||||
|
|
||||||
public MockSettingsInterface(string showHistory = "none", bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
|
||||||
|
|
||||||
|
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
||||||
{
|
{
|
||||||
_historyItems = mockHistory ?? new List<HistoryItem>();
|
_historyItems = mockHistory ?? new List<HistoryItem>();
|
||||||
GlobalIfURI = globalIfUri;
|
GlobalIfURI = globalIfUri;
|
||||||
ShowHistory = showHistory;
|
HistoryItemCount = historyItemCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ListItem> LoadHistory()
|
public void AddHistoryItem(HistoryItem historyItem)
|
||||||
{
|
|
||||||
var listItems = new List<ListItem>();
|
|
||||||
foreach (var historyItem in _historyItems)
|
|
||||||
{
|
|
||||||
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
|
|
||||||
{
|
|
||||||
Title = historyItem.SearchString,
|
|
||||||
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listItems.Reverse();
|
|
||||||
return listItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveHistory(HistoryItem historyItem)
|
|
||||||
{
|
{
|
||||||
if (historyItem is null)
|
if (historyItem is null)
|
||||||
{
|
{
|
||||||
@@ -50,19 +37,22 @@ public class MockSettingsInterface : ISettingsInterface
|
|||||||
_historyItems.Add(historyItem);
|
_historyItems.Add(historyItem);
|
||||||
|
|
||||||
// Simulate the same logic as SettingsManager
|
// Simulate the same logic as SettingsManager
|
||||||
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
|
if (HistoryItemCount > 0)
|
||||||
{
|
{
|
||||||
while (_historyItems.Count > maxHistoryItems)
|
while (_historyItems.Count > HistoryItemCount)
|
||||||
{
|
{
|
||||||
_historyItems.RemoveAt(0); // Remove the oldest item
|
_historyItems.RemoveAt(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method for testing
|
// Helper method for testing
|
||||||
public void ClearHistory()
|
public void ClearHistory()
|
||||||
{
|
{
|
||||||
_historyItems.Clear();
|
_historyItems.Clear();
|
||||||
|
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method for testing
|
// Helper method for testing
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task LoadHistoryReturnsExpectedItems()
|
public async Task HistoryReturnsExpectedItems()
|
||||||
{
|
{
|
||||||
// Setup
|
// Setup
|
||||||
var mockHistoryItems = new List<HistoryItem>
|
var mockHistoryItems = new List<HistoryItem>
|
||||||
@@ -54,7 +54,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
|||||||
new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
|
new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
|
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
|
||||||
|
|
||||||
var page = new WebSearchListPage(settings);
|
var page = new WebSearchListPage(settings);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task LoadHistoryMoreThanLimitation()
|
public async Task HistoryExceedingLimitReturnsMaxItems()
|
||||||
{
|
{
|
||||||
// Setup
|
// Setup
|
||||||
var mockHistoryItems = new List<HistoryItem>
|
var mockHistoryItems = new List<HistoryItem>
|
||||||
@@ -89,7 +89,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
|||||||
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
|
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
|
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
|
||||||
|
|
||||||
var page = new WebSearchListPage(settings);
|
var page = new WebSearchListPage(settings);
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task LoadHistoryWithDisableSetting()
|
public async Task HistoryWhenSetToNoneReturnEmptyList()
|
||||||
{
|
{
|
||||||
// Setup
|
// Setup
|
||||||
var mockHistoryItems = new List<HistoryItem>
|
var mockHistoryItems = new List<HistoryItem>
|
||||||
@@ -122,7 +122,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
|||||||
new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)),
|
new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "None");
|
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
|
||||||
|
|
||||||
var page = new WebSearchListPage(settings);
|
var page = new WebSearchListPage(settings);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// 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.Threading.Tasks;
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||||
|
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||||
|
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class SettingsManagerTests : CommandPaletteUnitTestBase
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public async Task HistoryChangedEventIsRaisedWhenItemIsAdded()
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
var settings = new MockSettingsInterface(historyItemCount: 5);
|
||||||
|
var page = new WebSearchListPage(settings);
|
||||||
|
|
||||||
|
var eventRaised = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
settings.HistoryChanged += Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
settings.AddHistoryItem(new HistoryItem("test event", DateTime.UtcNow));
|
||||||
|
await Task.Delay(50);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(eventRaised, "Expected HistoryChanged to be raised when saving history.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
settings.HistoryChanged -= Handler;
|
||||||
|
page.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void Handler(object s, EventArgs e) => eventRaised = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||||
<VersionMajor>0</VersionMajor>
|
<VersionMajor>0</VersionMajor>
|
||||||
<VersionMinor>4</VersionMinor>
|
<VersionMinor>5</VersionMinor>
|
||||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
|
||||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||||
using Microsoft.CmdPal.Ext.Apps.State;
|
using Microsoft.CmdPal.Ext.Apps.State;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
@@ -45,6 +43,28 @@ public partial class AllAppsCommandProvider : CommandProvider
|
|||||||
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
|
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 override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
|
||||||
|
|
||||||
public ICommandItem? LookupApp(string displayName)
|
public ICommandItem? LookupApp(string displayName)
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
|||||||
|
|
||||||
private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}";
|
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
|
#pragma warning disable SA1401 // Fields should be private
|
||||||
internal static AllAppsSettings Instance = new();
|
internal static AllAppsSettings Instance = new();
|
||||||
#pragma warning restore SA1401 // Fields should be private
|
#pragma warning restore SA1401 // Fields should be private
|
||||||
@@ -42,6 +52,14 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
|||||||
|
|
||||||
public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value;
|
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(
|
private readonly ToggleSetting _enableStartMenuSource = new(
|
||||||
Namespaced(nameof(EnableStartMenuSource)),
|
Namespaced(nameof(EnableStartMenuSource)),
|
||||||
Resources.enable_start_menu_source,
|
Resources.enable_start_menu_source,
|
||||||
@@ -87,6 +105,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
|||||||
Settings.Add(_enableDesktopSource);
|
Settings.Add(_enableDesktopSource);
|
||||||
Settings.Add(_enableRegistrySource);
|
Settings.Add(_enableRegistrySource);
|
||||||
Settings.Add(_enablePathEnvironmentVariableSource);
|
Settings.Add(_enablePathEnvironmentVariableSource);
|
||||||
|
Settings.Add(_searchResultLimitSource);
|
||||||
|
|
||||||
// Load settings from file upon initialization
|
// Load settings from file upon initialization
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
|
|||||||
@@ -133,17 +133,13 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
|
|
||||||
newCommands.Add(new Separator());
|
newCommands.Add(new Separator());
|
||||||
|
|
||||||
// 0x50 = P
|
|
||||||
// Full key chord would be Ctrl+P
|
|
||||||
var pinKeyChord = KeyChordHelpers.FromModifiers(true, false, false, false, 0x50, 0);
|
|
||||||
|
|
||||||
if (isPinned)
|
if (isPinned)
|
||||||
{
|
{
|
||||||
newCommands.Add(
|
newCommands.Add(
|
||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new UnpinAppCommand(this.AppIdentifier))
|
new UnpinAppCommand(this.AppIdentifier))
|
||||||
{
|
{
|
||||||
RequestedShortcut = pinKeyChord,
|
RequestedShortcut = KeyChords.TogglePin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -152,7 +148,7 @@ internal sealed partial class AppListItem : ListItem
|
|||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new PinAppCommand(this.AppIdentifier))
|
new PinAppCommand(this.AppIdentifier))
|
||||||
{
|
{
|
||||||
RequestedShortcut = pinKeyChord,
|
RequestedShortcut = KeyChords.TogglePin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +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.Properties;
|
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Ext.Apps.Commands;
|
|
||||||
|
|
||||||
internal sealed partial class CopyPathCommand : InvokableCommand
|
|
||||||
{
|
|
||||||
private readonly string _target;
|
|
||||||
|
|
||||||
public CopyPathCommand(string target)
|
|
||||||
{
|
|
||||||
Name = Resources.copy_path;
|
|
||||||
Icon = Icons.CopyIcon;
|
|
||||||
|
|
||||||
_target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly CompositeFormat CopyFailedFormat = CompositeFormat.Parse(Resources.copy_failed);
|
|
||||||
|
|
||||||
public override CommandResult Invoke()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ClipboardHelper.SetText(_target);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError("Copy failed: " + ex.Message);
|
|
||||||
return CommandResult.ShowToast(
|
|
||||||
new ToastArgs
|
|
||||||
{
|
|
||||||
Message = string.Format(CultureInfo.CurrentCulture, CopyFailedFormat, ex.Message),
|
|
||||||
Result = CommandResult.KeepOpen(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return CommandResult.ShowToast(Resources.copied_to_clipboard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,6 @@ internal sealed class Icons
|
|||||||
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
|
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
|
||||||
|
|
||||||
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
|
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
|
||||||
|
|
||||||
|
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Common.Helpers;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.Apps;
|
||||||
|
|
||||||
|
internal static class KeyChords
|
||||||
|
{
|
||||||
|
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
|
||||||
|
|
||||||
|
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
|
||||||
|
|
||||||
|
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
|
||||||
|
|
||||||
|
internal static KeyChord RunAsAdministrator { get; } = WellKnownKeyChords.RunAsAdministrator;
|
||||||
|
|
||||||
|
internal static KeyChord RunAsDifferentUser { get; } = WellKnownKeyChords.RunAsDifferentUser;
|
||||||
|
|
||||||
|
internal static KeyChord TogglePin { get; } = WellKnownKeyChords.TogglePin;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||||
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
|
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
|
||||||
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
|
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ public class UWPApplication : IUWPApplication
|
|||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true))
|
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true))
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter),
|
RequestedShortcut = KeyChords.RunAsAdministrator,
|
||||||
});
|
});
|
||||||
|
|
||||||
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
|
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
|
||||||
@@ -95,9 +95,9 @@ public class UWPApplication : IUWPApplication
|
|||||||
|
|
||||||
commands.Add(
|
commands.Add(
|
||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new Commands.CopyPathCommand(Location))
|
new CopyTextCommand(Location) { Name = Resources.copy_path })
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
|
RequestedShortcut = KeyChords.CopyFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.Add(
|
commands.Add(
|
||||||
@@ -107,16 +107,24 @@ public class UWPApplication : IUWPApplication
|
|||||||
Name = Resources.open_containing_folder,
|
Name = Resources.open_containing_folder,
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E),
|
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.Add(
|
commands.Add(
|
||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new OpenInConsoleCommand(Package.Location))
|
new OpenInConsoleCommand(Package.Location))
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
RequestedShortcut = KeyChords.OpenInConsole,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
commands.Add(
|
||||||
|
new CommandContextItem(
|
||||||
|
new UninstallApplicationConfirmation(this))
|
||||||
|
{
|
||||||
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
|
||||||
|
IsCritical = true,
|
||||||
|
});
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,34 +191,44 @@ public partial class Win32Program : IProgram
|
|||||||
commands.Add(new CommandContextItem(
|
commands.Add(new CommandContextItem(
|
||||||
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))
|
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter),
|
RequestedShortcut = KeyChords.RunAsAdministrator,
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.Add(new CommandContextItem(
|
commands.Add(new CommandContextItem(
|
||||||
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))
|
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U),
|
RequestedShortcut = KeyChords.RunAsDifferentUser,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.Add(new CommandContextItem(
|
commands.Add(new CommandContextItem(
|
||||||
new Commands.CopyPathCommand(FullPath))
|
new CopyTextCommand(FullPath) { Name = Resources.copy_path })
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
|
RequestedShortcut = KeyChords.CopyFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.Add(new CommandContextItem(
|
commands.Add(new CommandContextItem(
|
||||||
new OpenPathCommand(ParentDirectory))
|
new OpenPathCommand(ParentDirectory))
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E),
|
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.Add(new CommandContextItem(
|
commands.Add(new CommandContextItem(
|
||||||
new OpenInConsoleCommand(ParentDirectory))
|
new OpenInConsoleCommand(ParentDirectory))
|
||||||
{
|
{
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
RequestedShortcut = KeyChords.OpenInConsole,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,24 +78,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Copied to clipboard!.
|
|
||||||
/// </summary>
|
|
||||||
internal static string copied_to_clipboard {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("copied_to_clipboard", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Copy failed ({0}). Please try again..
|
|
||||||
/// </summary>
|
|
||||||
internal static string copy_failed {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("copy_failed", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Copy path.
|
/// Looks up a localized string similar to Copy path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -177,6 +159,78 @@ 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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Open containing folder.
|
/// Looks up a localized string similar to Open containing folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -276,6 +330,51 @@ 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 "{0}"?.
|
||||||
|
/// </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 '{0}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application_failed {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application_failed", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to '{0}' has been successfully uninstalled..
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application_successful {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application_successful", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Unpin.
|
/// Looks up a localized string similar to Unpin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -172,13 +172,6 @@
|
|||||||
<data name="run_as_different_user" xml:space="preserve">
|
<data name="run_as_different_user" xml:space="preserve">
|
||||||
<value>Run as different user</value>
|
<value>Run as different user</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="copy_failed" xml:space="preserve">
|
|
||||||
<value>Copy failed ({0}). Please try again.</value>
|
|
||||||
<comment>{0} is the error message</comment>
|
|
||||||
</data>
|
|
||||||
<data name="copied_to_clipboard" xml:space="preserve">
|
|
||||||
<value>Copied to clipboard!</value>
|
|
||||||
</data>
|
|
||||||
<data name="enable_start_menu_source" xml:space="preserve">
|
<data name="enable_start_menu_source" xml:space="preserve">
|
||||||
<value>Include apps found in the Start Menu</value>
|
<value>Include apps found in the Start Menu</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -205,4 +198,43 @@
|
|||||||
<data name="unpin_app" xml:space="preserve">
|
<data name="unpin_app" xml:space="preserve">
|
||||||
<value>Unpin</value>
|
<value>Unpin</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<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>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -11,19 +12,25 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
|||||||
public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||||
{
|
{
|
||||||
private readonly ListItem _clipboardHistoryListItem;
|
private readonly ListItem _clipboardHistoryListItem;
|
||||||
|
private readonly SettingsManager _settingsManager = new();
|
||||||
|
|
||||||
public ClipboardHistoryCommandsProvider()
|
public ClipboardHistoryCommandsProvider()
|
||||||
{
|
{
|
||||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
|
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
|
||||||
{
|
{
|
||||||
Title = Properties.Resources.list_item_title,
|
Title = Properties.Resources.list_item_title,
|
||||||
Subtitle = Properties.Resources.list_item_subtitle,
|
Subtitle = Properties.Resources.list_item_subtitle,
|
||||||
Icon = Icons.ClipboardListIcon,
|
Icon = Icons.ClipboardListIcon,
|
||||||
|
MoreCommands = [
|
||||||
|
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
DisplayName = Properties.Resources.provider_display_name;
|
DisplayName = Properties.Resources.provider_display_name;
|
||||||
Icon = Icons.ClipboardListIcon;
|
Icon = Icons.ClipboardListIcon;
|
||||||
Id = "Windows.ClipboardHistory";
|
Id = "Windows.ClipboardHistory";
|
||||||
|
|
||||||
|
Settings = _settingsManager.Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IListItem[] TopLevelCommands()
|
public override IListItem[] TopLevelCommands()
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||||
|
|
||||||
|
internal sealed partial class DeleteItemCommand : InvokableCommand
|
||||||
|
{
|
||||||
|
private readonly ClipboardItem _clipboardItem;
|
||||||
|
|
||||||
|
internal DeleteItemCommand(ClipboardItem clipboardItem)
|
||||||
|
{
|
||||||
|
_clipboardItem = clipboardItem;
|
||||||
|
Name = Properties.Resources.delete_command_name;
|
||||||
|
Icon = Icons.DeleteIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override CommandResult Invoke()
|
||||||
|
{
|
||||||
|
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||||
|
return CommandResult.ShowToast(new ToastArgs
|
||||||
|
{
|
||||||
|
Message = Properties.Resources.delete_toast_text,
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Common.Messages;
|
using Microsoft.CmdPal.Common.Messages;
|
||||||
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
@@ -14,11 +15,13 @@ internal sealed partial class PasteCommand : InvokableCommand
|
|||||||
{
|
{
|
||||||
private readonly ClipboardItem _clipboardItem;
|
private readonly ClipboardItem _clipboardItem;
|
||||||
private readonly ClipboardFormat _clipboardFormat;
|
private readonly ClipboardFormat _clipboardFormat;
|
||||||
|
private readonly ISettingOptions _settings;
|
||||||
|
|
||||||
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
|
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat, ISettingOptions settings)
|
||||||
{
|
{
|
||||||
_clipboardItem = clipboardItem;
|
_clipboardItem = clipboardItem;
|
||||||
_clipboardFormat = clipboardFormat;
|
_clipboardFormat = clipboardFormat;
|
||||||
|
_settings = settings;
|
||||||
Name = Properties.Resources.paste_command_name;
|
Name = Properties.Resources.paste_command_name;
|
||||||
Icon = Icons.PasteIcon;
|
Icon = Icons.PasteIcon;
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,11 @@ internal sealed partial class PasteCommand : InvokableCommand
|
|||||||
|
|
||||||
ClipboardHelper.SendPasteKeyCombination();
|
ClipboardHelper.SendPasteKeyCombination();
|
||||||
|
|
||||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
if (!_settings.KeepAfterPaste)
|
||||||
|
{
|
||||||
|
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||||
|
}
|
||||||
|
|
||||||
return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
|
return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||||
|
|
||||||
|
public interface ISettingOptions
|
||||||
|
{
|
||||||
|
bool KeepAfterPaste { get; }
|
||||||
|
|
||||||
|
bool DeleteFromHistoryRequiresConfirmation { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Properties;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||||
|
|
||||||
|
internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
|
||||||
|
{
|
||||||
|
private const string Namespace = "clipboardHistory";
|
||||||
|
|
||||||
|
private static string Namespaced(string propertyName) => $"{Namespace}.{propertyName}";
|
||||||
|
|
||||||
|
private readonly ToggleSetting _keepAfterPaste = new(
|
||||||
|
Namespaced(nameof(KeepAfterPaste)),
|
||||||
|
Resources.settings_keep_after_paste_title!,
|
||||||
|
Resources.settings_keep_after_paste_description!,
|
||||||
|
false);
|
||||||
|
|
||||||
|
private readonly ToggleSetting _confirmDelete = new(
|
||||||
|
Namespaced(nameof(DeleteFromHistoryRequiresConfirmation)),
|
||||||
|
Resources.settings_confirm_delete_title!,
|
||||||
|
Resources.settings_confirm_delete_description!,
|
||||||
|
true);
|
||||||
|
|
||||||
|
public bool KeepAfterPaste => _keepAfterPaste.Value;
|
||||||
|
|
||||||
|
public bool DeleteFromHistoryRequiresConfirmation => _confirmDelete.Value;
|
||||||
|
|
||||||
|
private static string SettingsJsonPath()
|
||||||
|
{
|
||||||
|
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
|
// now, the state is just next to the exe
|
||||||
|
return Path.Combine(directory, "settings.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingsManager()
|
||||||
|
{
|
||||||
|
FilePath = SettingsJsonPath();
|
||||||
|
|
||||||
|
Settings.Add(_keepAfterPaste);
|
||||||
|
Settings.Add(_confirmDelete);
|
||||||
|
|
||||||
|
// Load settings from file upon initialization
|
||||||
|
LoadSettings();
|
||||||
|
|
||||||
|
Settings.SettingsChanged += (_, _) => SaveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,5 +14,7 @@ internal sealed class Icons
|
|||||||
|
|
||||||
internal static IconInfo PasteIcon { get; } = new("\uE77F");
|
internal static IconInfo PasteIcon { get; } = new("\uE77F");
|
||||||
|
|
||||||
|
internal static IconInfo DeleteIcon { get; } = new("\uE74D");
|
||||||
|
|
||||||
internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg");
|
internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||||
|
|
||||||
|
internal static class KeyChords
|
||||||
|
{
|
||||||
|
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.CmdPal.Common.Commands;
|
||||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||||
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
using Windows.Storage.Streams;
|
using Windows.Storage.Streams;
|
||||||
@@ -16,9 +18,11 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
|||||||
|
|
||||||
public class ClipboardItem
|
public class ClipboardItem
|
||||||
{
|
{
|
||||||
public string? Content { get; set; }
|
public string? Content { get; init; }
|
||||||
|
|
||||||
public required ClipboardHistoryItem Item { get; set; }
|
public required ClipboardHistoryItem Item { get; init; }
|
||||||
|
|
||||||
|
public required ISettingOptions Settings { get; init; }
|
||||||
|
|
||||||
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
|
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
|
||||||
|
|
||||||
@@ -87,6 +91,19 @@ public class ClipboardItem
|
|||||||
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
|
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var deleteConfirmationCommand = new ConfirmableCommand()
|
||||||
|
{
|
||||||
|
Command = new DeleteItemCommand(this),
|
||||||
|
ConfirmationTitle = Properties.Resources.delete_confirmation_title!,
|
||||||
|
ConfirmationMessage = Properties.Resources.delete_confirmation_message!,
|
||||||
|
IsConfirmationRequired = () => Settings.DeleteFromHistoryRequiresConfirmation,
|
||||||
|
};
|
||||||
|
var deleteContextMenuItem = new CommandContextItem(deleteConfirmationCommand)
|
||||||
|
{
|
||||||
|
IsCritical = true,
|
||||||
|
RequestedShortcut = KeyChords.DeleteEntry,
|
||||||
|
};
|
||||||
|
|
||||||
if (IsImage)
|
if (IsImage)
|
||||||
{
|
{
|
||||||
var iconData = new IconData(ImageData);
|
var iconData = new IconData(ImageData);
|
||||||
@@ -103,7 +120,9 @@ public class ClipboardItem
|
|||||||
Metadata = metadata.ToArray(),
|
Metadata = metadata.ToArray(),
|
||||||
},
|
},
|
||||||
MoreCommands = [
|
MoreCommands = [
|
||||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image))
|
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image, Settings)),
|
||||||
|
new Separator(),
|
||||||
|
deleteContextMenuItem,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -126,8 +145,10 @@ public class ClipboardItem
|
|||||||
Metadata = metadata.ToArray(),
|
Metadata = metadata.ToArray(),
|
||||||
},
|
},
|
||||||
MoreCommands = [
|
MoreCommands = [
|
||||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)),
|
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text, Settings)),
|
||||||
],
|
new Separator(),
|
||||||
|
deleteContextMenuItem,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -17,11 +18,15 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
|||||||
|
|
||||||
internal sealed partial class ClipboardHistoryListPage : ListPage
|
internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||||
{
|
{
|
||||||
|
private readonly SettingsManager _settingsManager;
|
||||||
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
|
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
|
||||||
private readonly string _defaultIconPath;
|
private readonly string _defaultIconPath;
|
||||||
|
|
||||||
public ClipboardHistoryListPage()
|
public ClipboardHistoryListPage(SettingsManager settingsManager)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(settingsManager);
|
||||||
|
|
||||||
|
_settingsManager = settingsManager;
|
||||||
clipboardHistory = [];
|
clipboardHistory = [];
|
||||||
_defaultIconPath = string.Empty;
|
_defaultIconPath = string.Empty;
|
||||||
Icon = Icons.ClipboardListIcon;
|
Icon = Icons.ClipboardListIcon;
|
||||||
@@ -84,11 +89,11 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
|
|||||||
if (item.Content.Contains(StandardDataFormats.Text))
|
if (item.Content.Contains(StandardDataFormats.Text))
|
||||||
{
|
{
|
||||||
var text = await item.Content.GetTextAsync();
|
var text = await item.Content.GetTextAsync();
|
||||||
items.Add(new ClipboardItem { Content = text, Item = item });
|
items.Add(new ClipboardItem { Settings = _settingsManager, Content = text, Item = item });
|
||||||
}
|
}
|
||||||
else if (item.Content.Contains(StandardDataFormats.Bitmap))
|
else if (item.Content.Contains(StandardDataFormats.Bitmap))
|
||||||
{
|
{
|
||||||
items.Add(new ClipboardItem { Item = item });
|
items.Add(new ClipboardItem { Settings = _settingsManager, Item = item });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,42 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Delete.
|
||||||
|
/// </summary>
|
||||||
|
public static string delete_command_name {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("delete_command_name", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Are you sure you want to delete this item from clipboard history? This action cannot be undone..
|
||||||
|
/// </summary>
|
||||||
|
public static string delete_confirmation_message {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("delete_confirmation_message", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Delete item?.
|
||||||
|
/// </summary>
|
||||||
|
public static string delete_confirmation_title {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("delete_confirmation_title", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Deleted from clipboard history.
|
||||||
|
/// </summary>
|
||||||
|
public static string delete_toast_text {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("delete_toast_text", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
|
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -140,5 +176,41 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
|||||||
return ResourceManager.GetString("provider_display_name", resourceCulture);
|
return ResourceManager.GetString("provider_display_name", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to .
|
||||||
|
/// </summary>
|
||||||
|
public static string settings_confirm_delete_description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("settings_confirm_delete_description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Show a confirmation dialog when manually deleting an item.
|
||||||
|
/// </summary>
|
||||||
|
public static string settings_confirm_delete_title {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("settings_confirm_delete_title", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to .
|
||||||
|
/// </summary>
|
||||||
|
public static string settings_keep_after_paste_description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("settings_keep_after_paste_description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Keep items in clipboard history after pasting.
|
||||||
|
/// </summary>
|
||||||
|
public static string settings_keep_after_paste_title {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("settings_keep_after_paste_title", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,4 +144,28 @@
|
|||||||
<data name="clipboard_failed_to_load" xml:space="preserve">
|
<data name="clipboard_failed_to_load" xml:space="preserve">
|
||||||
<value>Loading clipboard history failed</value>
|
<value>Loading clipboard history failed</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="delete_command_name" xml:space="preserve">
|
||||||
|
<value>Delete</value>
|
||||||
|
</data>
|
||||||
|
<data name="delete_toast_text" xml:space="preserve">
|
||||||
|
<value>Deleted from clipboard history</value>
|
||||||
|
</data>
|
||||||
|
<data name="settings_keep_after_paste_description" xml:space="preserve">
|
||||||
|
<value />
|
||||||
|
</data>
|
||||||
|
<data name="settings_keep_after_paste_title" xml:space="preserve">
|
||||||
|
<value>Keep items in clipboard history after pasting</value>
|
||||||
|
</data>
|
||||||
|
<data name="settings_confirm_delete_title" xml:space="preserve">
|
||||||
|
<value>Show a confirmation dialog when manually deleting an item</value>
|
||||||
|
</data>
|
||||||
|
<data name="settings_confirm_delete_description" xml:space="preserve">
|
||||||
|
<value />
|
||||||
|
</data>
|
||||||
|
<data name="delete_confirmation_title" xml:space="preserve">
|
||||||
|
<value>Delete item?</value>
|
||||||
|
</data>
|
||||||
|
<data name="delete_confirmation_message" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to delete this item from clipboard history? This action cannot be undone.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -91,9 +91,9 @@ internal sealed partial class IndexerListItem : ListItem
|
|||||||
}
|
}
|
||||||
|
|
||||||
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
|
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
|
||||||
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }));
|
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }) { RequestedShortcut = KeyChords.OpenFileLocation });
|
||||||
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }));
|
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }) { RequestedShortcut = KeyChords.CopyFilePath });
|
||||||
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)));
|
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)) { RequestedShortcut = KeyChords.OpenInConsole });
|
||||||
commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath)));
|
commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath)));
|
||||||
|
|
||||||
if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))
|
if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Common.Helpers;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||||
|
|
||||||
|
internal static class KeyChords
|
||||||
|
{
|
||||||
|
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
|
||||||
|
|
||||||
|
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
|
||||||
|
|
||||||
|
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
|
|||||||
{
|
{
|
||||||
Title = string.Empty;
|
Title = string.Empty;
|
||||||
Subtitle = string.Empty;
|
Subtitle = string.Empty;
|
||||||
|
Icon = Icons.LockIcon;
|
||||||
|
|
||||||
var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi;
|
var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi;
|
||||||
var hideEmptyRB = settings.HideEmptyRecycleBin();
|
var hideEmptyRB = settings.HideEmptyRecycleBin();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
|
|||||||
{
|
{
|
||||||
Title = string.Empty;
|
Title = string.Empty;
|
||||||
Subtitle = string.Empty;
|
Subtitle = string.Empty;
|
||||||
|
Icon = Icons.TimeDateIcon;
|
||||||
_settingsManager = settings;
|
_settingsManager = settings;
|
||||||
_timestamp = timestamp;
|
_timestamp = timestamp;
|
||||||
|
|
||||||
|
|||||||
@@ -79,16 +79,22 @@ public sealed partial class TimeDateCalculator
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
List<(int Score, AvailableResult Item)> itemScores = [];
|
||||||
|
|
||||||
// Generate filtered list of results
|
// Generate filtered list of results
|
||||||
foreach (var f in availableFormats)
|
foreach (var f in availableFormats)
|
||||||
{
|
{
|
||||||
var score = f.Score(query, f.Label, f.AlternativeSearchTag);
|
var score = f.Score(query, f.Label, f.AlternativeSearchTag);
|
||||||
|
|
||||||
if (score > 0)
|
if (score > 0)
|
||||||
{
|
{
|
||||||
results.Add(f.ToListItem());
|
itemScores.Add((score, f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results = itemScores
|
||||||
|
.OrderByDescending(s => s.Score)
|
||||||
|
.Select(s => s.Item.ToListItem())
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.Count == 0)
|
if (results.Count == 0)
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ internal sealed partial class SearchWebCommand : InvokableCommand
|
|||||||
return CommandResult.KeepOpen();
|
return CommandResult.KeepOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_settingsManager.ShowHistory != Resources.history_none)
|
if (_settingsManager.HistoryItemCount != 0)
|
||||||
{
|
{
|
||||||
_settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now));
|
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandResult.Dismiss();
|
return CommandResult.Dismiss();
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using ManagedCommon;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||||
|
|
||||||
|
internal sealed class HistoryStore
|
||||||
|
{
|
||||||
|
private readonly string _filePath;
|
||||||
|
private readonly List<HistoryItem> _items = [];
|
||||||
|
private readonly Lock _lock = new();
|
||||||
|
|
||||||
|
private int _capacity;
|
||||||
|
|
||||||
|
public event EventHandler? Changed;
|
||||||
|
|
||||||
|
public HistoryStore(string filePath, int capacity)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(filePath);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||||
|
|
||||||
|
_filePath = filePath;
|
||||||
|
_capacity = capacity;
|
||||||
|
|
||||||
|
_items.AddRange(LoadFromDiskSafe());
|
||||||
|
TrimNoLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<HistoryItem> HistoryItems
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return [.. _items];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(HistoryItem item)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_items.Add(item);
|
||||||
|
_ = TrimNoLock();
|
||||||
|
SaveNoLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
Changed?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCapacity(int capacity)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||||
|
|
||||||
|
bool trimmed;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_capacity = capacity;
|
||||||
|
trimmed = TrimNoLock();
|
||||||
|
if (trimmed)
|
||||||
|
{
|
||||||
|
SaveNoLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed)
|
||||||
|
{
|
||||||
|
Changed?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TrimNoLock()
|
||||||
|
{
|
||||||
|
var max = _capacity;
|
||||||
|
if (_items.Count > max)
|
||||||
|
{
|
||||||
|
_items.RemoveRange(0, _items.Count - max);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HistoryItem> LoadFromDiskSafe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_filePath))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileContent = File.ReadAllText(_filePath);
|
||||||
|
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
||||||
|
return historyItems;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Unable to load history", ex);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveNoLock()
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(_items, WebSearchJsonSerializationContext.Default.ListHistoryItem);
|
||||||
|
File.WriteAllText(_filePath, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
@@ -9,11 +10,13 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
|||||||
|
|
||||||
public interface ISettingsInterface
|
public interface ISettingsInterface
|
||||||
{
|
{
|
||||||
|
event EventHandler? HistoryChanged;
|
||||||
|
|
||||||
public bool GlobalIfURI { get; }
|
public bool GlobalIfURI { get; }
|
||||||
|
|
||||||
public string ShowHistory { get; }
|
public int HistoryItemCount { get; }
|
||||||
|
|
||||||
public List<ListItem> LoadHistory();
|
public IReadOnlyList<HistoryItem> HistoryItems { get; }
|
||||||
|
|
||||||
public void SaveHistory(HistoryItem historyItem);
|
public void AddHistoryItem(HistoryItem historyItem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using ManagedCommon;
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
@@ -16,19 +13,26 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
|||||||
|
|
||||||
public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||||
{
|
{
|
||||||
private readonly string _historyPath;
|
private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
|
||||||
|
|
||||||
private static readonly string _namespace = "websearch";
|
private static readonly string _namespace = "websearch";
|
||||||
|
|
||||||
|
public event EventHandler? HistoryChanged
|
||||||
|
{
|
||||||
|
add => _history.Changed += value;
|
||||||
|
remove => _history.Changed -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly HistoryStore _history;
|
||||||
|
|
||||||
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
|
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
|
||||||
|
|
||||||
private static readonly List<ChoiceSetSetting.Choice> _choices =
|
private static readonly List<ChoiceSetSetting.Choice> _choices =
|
||||||
[
|
[
|
||||||
new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none),
|
new ChoiceSetSetting.Choice(Resources.history_none, "None"),
|
||||||
new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1),
|
new ChoiceSetSetting.Choice(Resources.history_1, "1"),
|
||||||
new ChoiceSetSetting.Choice(Resources.history_5, Resources.history_5),
|
new ChoiceSetSetting.Choice(Resources.history_5, "5"),
|
||||||
new ChoiceSetSetting.Choice(Resources.history_10, Resources.history_10),
|
new ChoiceSetSetting.Choice(Resources.history_10, "10"),
|
||||||
new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20),
|
new ChoiceSetSetting.Choice(Resources.history_20, "20"),
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly ToggleSetting _globalIfURI = new(
|
private readonly ToggleSetting _globalIfURI = new(
|
||||||
@@ -37,17 +41,34 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
|||||||
Resources.plugin_global_if_uri,
|
Resources.plugin_global_if_uri,
|
||||||
false);
|
false);
|
||||||
|
|
||||||
private readonly ChoiceSetSetting _showHistory = new(
|
private readonly ChoiceSetSetting _historyItemCount = new(
|
||||||
Namespaced(nameof(ShowHistory)),
|
Namespaced(HistoryItemCountLegacySettingsKey),
|
||||||
Resources.plugin_show_history,
|
Resources.plugin_history_item_count,
|
||||||
Resources.plugin_show_history,
|
Resources.plugin_history_item_count,
|
||||||
_choices);
|
_choices);
|
||||||
|
|
||||||
public bool GlobalIfURI => _globalIfURI.Value;
|
public bool GlobalIfURI => _globalIfURI.Value;
|
||||||
|
|
||||||
public string ShowHistory => _showHistory.Value ?? string.Empty;
|
public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
|
||||||
|
|
||||||
internal static string SettingsJsonPath()
|
public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
|
||||||
|
|
||||||
|
public SettingsManager()
|
||||||
|
{
|
||||||
|
FilePath = SettingsJsonPath();
|
||||||
|
|
||||||
|
Settings.Add(_globalIfURI);
|
||||||
|
Settings.Add(_historyItemCount);
|
||||||
|
|
||||||
|
LoadSettings();
|
||||||
|
|
||||||
|
// Initialize history store after loading settings to get the correct capacity
|
||||||
|
_history = new HistoryStore(HistoryStateJsonPath(), HistoryItemCount);
|
||||||
|
|
||||||
|
Settings.SettingsChanged += (_, _) => SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SettingsJsonPath()
|
||||||
{
|
{
|
||||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
@@ -56,7 +77,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
|||||||
return Path.Combine(directory, "settings.json");
|
return Path.Combine(directory, "settings.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string HistoryStateJsonPath()
|
private static string HistoryStateJsonPath()
|
||||||
{
|
{
|
||||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
@@ -65,156 +86,30 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
|||||||
return Path.Combine(directory, "websearch_history.json");
|
return Path.Combine(directory, "websearch_history.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveHistory(HistoryItem historyItem)
|
public void AddHistoryItem(HistoryItem historyItem)
|
||||||
{
|
{
|
||||||
if (historyItem is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<HistoryItem> historyItems;
|
_history.Add(historyItem);
|
||||||
|
|
||||||
// Check if the file exists and load existing history
|
|
||||||
if (File.Exists(_historyPath))
|
|
||||||
{
|
|
||||||
var existingContent = File.ReadAllText(_historyPath);
|
|
||||||
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
historyItems = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the new history item
|
|
||||||
historyItems.Add(historyItem);
|
|
||||||
|
|
||||||
// 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 > maxHistoryItems)
|
|
||||||
{
|
|
||||||
historyItems.RemoveAt(0); // Remove the oldest item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the updated list back to JSON and save it
|
|
||||||
var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
|
|
||||||
File.WriteAllText(_historyPath, historyJson);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Logger.LogError("Failed to add item to the search history", ex);
|
||||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ListItem> LoadHistory()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(_historyPath))
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and deserialize JSON into a list of HistoryItem objects
|
|
||||||
var fileContent = File.ReadAllText(_historyPath);
|
|
||||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
|
||||||
|
|
||||||
// Convert each HistoryItem to a ListItem
|
|
||||||
var listItems = new List<ListItem>();
|
|
||||||
foreach (var historyItem in historyItems)
|
|
||||||
{
|
|
||||||
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
|
|
||||||
{
|
|
||||||
Title = historyItem.SearchString,
|
|
||||||
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture), // Ensures consistent formatting
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listItems.Reverse();
|
|
||||||
return listItems;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsManager()
|
|
||||||
{
|
|
||||||
FilePath = SettingsJsonPath();
|
|
||||||
_historyPath = HistoryStateJsonPath();
|
|
||||||
|
|
||||||
Settings.Add(_globalIfURI);
|
|
||||||
Settings.Add(_showHistory);
|
|
||||||
|
|
||||||
// Load settings from file upon initialization
|
|
||||||
LoadSettings();
|
|
||||||
|
|
||||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearHistory()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(_historyPath))
|
|
||||||
{
|
|
||||||
// Delete the history file
|
|
||||||
File.Delete(_historyPath);
|
|
||||||
|
|
||||||
// Log that the history was successfully cleared
|
|
||||||
ExtensionHost.LogMessage(new LogMessage() { Message = "History cleared successfully." });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Log that there was no history file to delete
|
|
||||||
ExtensionHost.LogMessage(new LogMessage() { Message = "No history file found to clear." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log any exception that occurs
|
|
||||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to clear history: {ex}" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SaveSettings()
|
public override void SaveSettings()
|
||||||
{
|
{
|
||||||
base.SaveSettings();
|
base.SaveSettings();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (ShowHistory == Resources.history_none)
|
_history.SetCapacity(HistoryItemCount);
|
||||||
{
|
|
||||||
ClearHistory();
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
var existingContent = File.ReadAllText(_historyPath);
|
|
||||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
|
||||||
|
|
||||||
// Check if trimming is needed
|
|
||||||
if (historyItems.Count > maxHistoryItems)
|
|
||||||
{
|
|
||||||
// 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);
|
|
||||||
File.WriteAllText(_historyPath, trimmedHistoryJson);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Logger.LogError("Failed to save the search history", ex);
|
||||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||||
@@ -16,31 +16,30 @@ using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
|
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||||
|
|
||||||
internal sealed partial class WebSearchListPage : DynamicListPage
|
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _iconPath = string.Empty;
|
private readonly IconInfo _newSearchIcon = new(string.Empty);
|
||||||
private readonly List<ListItem>? _historyItems;
|
|
||||||
private readonly ISettingsInterface _settingsManager;
|
private readonly ISettingsInterface _settingsManager;
|
||||||
|
private readonly Lock _sync = new();
|
||||||
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
|
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
|
||||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||||
private List<ListItem> _allItems;
|
private IListItem[] _allItems = [];
|
||||||
|
private List<ListItem> _historyItems = [];
|
||||||
|
|
||||||
public WebSearchListPage(ISettingsInterface settingsManager)
|
public WebSearchListPage(ISettingsInterface settingsManager)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(settingsManager);
|
||||||
|
|
||||||
Name = Resources.command_item_title;
|
Name = Resources.command_item_title;
|
||||||
Title = Resources.command_item_title;
|
Title = Resources.command_item_title;
|
||||||
Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
|
Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
|
||||||
_allItems = [];
|
|
||||||
Id = "com.microsoft.cmdpal.websearch";
|
Id = "com.microsoft.cmdpal.websearch";
|
||||||
|
|
||||||
_settingsManager = settingsManager;
|
_settingsManager = settingsManager;
|
||||||
_historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null;
|
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
|
||||||
if (_historyItems is not null)
|
|
||||||
{
|
|
||||||
_allItems.AddRange(_historyItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// It just looks viewer to have string twice on the page, and default placeholder is good enough
|
// It just looks viewer to have string twice on the page, and default placeholder is good enough
|
||||||
PlaceholderText = _allItems.Count > 0 ? Resources.plugin_description : string.Empty;
|
PlaceholderText = _allItems.Length > 0 ? Resources.plugin_description : string.Empty;
|
||||||
|
|
||||||
EmptyContent = new CommandItem(new NoOpCommand())
|
EmptyContent = new CommandItem(new NoOpCommand())
|
||||||
{
|
{
|
||||||
@@ -48,45 +47,102 @@ internal sealed partial class WebSearchListPage : DynamicListPage
|
|||||||
Title = Properties.Resources.plugin_description,
|
Title = Properties.Resources.plugin_description,
|
||||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UpdateHistory();
|
||||||
|
RequeryAndUpdateItems(SearchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ListItem> Query(string query)
|
private void SettingsManagerOnHistoryChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
UpdateHistory();
|
||||||
IEnumerable<ListItem>? filteredHistoryItems = null;
|
RequeryAndUpdateItems(SearchText);
|
||||||
|
}
|
||||||
|
|
||||||
if (_historyItems is not null)
|
private void UpdateHistory()
|
||||||
|
{
|
||||||
|
List<ListItem> history = [];
|
||||||
|
|
||||||
|
if (_settingsManager.HistoryItemCount > 0)
|
||||||
{
|
{
|
||||||
filteredHistoryItems = _settingsManager.ShowHistory != Resources.history_none ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null;
|
var items = _settingsManager.HistoryItems;
|
||||||
|
for (var index = items.Count - 1; index >= 0; index--)
|
||||||
|
{
|
||||||
|
var historyItem = items[index];
|
||||||
|
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
|
||||||
|
{
|
||||||
|
Title = historyItem.SearchString,
|
||||||
|
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = new List<ListItem>();
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_historyItems = history;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IconInfo newSearchIcon)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
|
var filteredHistoryItems = settingsManager.HistoryItemCount > 0
|
||||||
|
? ListHelpers.FilterList(historySnapshot, query)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
var results = new List<IListItem>();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(query))
|
if (!string.IsNullOrEmpty(query))
|
||||||
{
|
{
|
||||||
var searchTerm = query;
|
var searchTerm = query;
|
||||||
var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager))
|
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
|
||||||
{
|
{
|
||||||
Title = searchTerm,
|
Title = searchTerm,
|
||||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||||
Icon = new IconInfo(_iconPath),
|
Icon = newSearchIcon,
|
||||||
};
|
};
|
||||||
results.Add(result);
|
results.Add(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredHistoryItems is not null)
|
results.AddRange(filteredHistoryItems);
|
||||||
|
|
||||||
|
return [.. results];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RequeryAndUpdateItems(string search)
|
||||||
|
{
|
||||||
|
List<ListItem> historySnapshot;
|
||||||
|
lock (_sync)
|
||||||
{
|
{
|
||||||
results.AddRange(filteredHistoryItems);
|
historySnapshot = _historyItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _newSearchIcon);
|
||||||
|
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_allItems = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
RaiseItemsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||||
{
|
{
|
||||||
_allItems = [.. Query(newSearch)];
|
RequeryAndUpdateItems(newSearch);
|
||||||
RaiseItemsChanged(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IListItem[] GetItems() => [.. _allItems];
|
public override IListItem[] GetItems()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
return _allItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_settingsManager.HistoryChanged -= SettingsManagerOnHistoryChanged;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,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_history_item_count {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("plugin_history_item_count", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to In the default browser.
|
/// Looks up a localized string similar to In the default browser.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -231,15 +240,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_show_history {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("plugin_show_history", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Settings.
|
/// Looks up a localized string similar to Settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
<data name="plugin_search_failed" xml:space="preserve">
|
<data name="plugin_search_failed" xml:space="preserve">
|
||||||
<value>Failed to open {0}.</value>
|
<value>Failed to open {0}.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="plugin_show_history" xml:space="preserve">
|
<data name="plugin_history_item_count" xml:space="preserve">
|
||||||
<value>Determines the number of history items to show from previous searches</value>
|
<value>Determines the number of history items to show from previous searches</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="settings_page_name" xml:space="preserve">
|
<data name="settings_page_name" xml:space="preserve">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||||
@@ -15,6 +16,9 @@ public partial class WebSearchCommandsProvider : CommandProvider
|
|||||||
private readonly SettingsManager _settingsManager = new();
|
private readonly SettingsManager _settingsManager = new();
|
||||||
private readonly FallbackExecuteSearchItem _fallbackItem;
|
private readonly FallbackExecuteSearchItem _fallbackItem;
|
||||||
private readonly FallbackOpenURLItem _openUrlFallbackItem;
|
private readonly FallbackOpenURLItem _openUrlFallbackItem;
|
||||||
|
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
|
||||||
|
private readonly ICommandItem[] _topLevelItems;
|
||||||
|
private readonly IFallbackCommandItem[] _fallbackCommands;
|
||||||
|
|
||||||
public WebSearchCommandsProvider()
|
public WebSearchCommandsProvider()
|
||||||
{
|
{
|
||||||
@@ -25,18 +29,27 @@ public partial class WebSearchCommandsProvider : CommandProvider
|
|||||||
|
|
||||||
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
|
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
|
||||||
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
|
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
|
||||||
}
|
|
||||||
|
|
||||||
public override ICommandItem[] TopLevelCommands()
|
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
|
||||||
{
|
|
||||||
return [new WebSearchTopLevelCommandItem(_settingsManager)
|
|
||||||
{
|
{
|
||||||
MoreCommands = [
|
MoreCommands =
|
||||||
|
[
|
||||||
new CommandContextItem(Settings!.SettingsPage),
|
new CommandContextItem(Settings!.SettingsPage),
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
];
|
_topLevelItems = [_webSearchTopLevelItem];
|
||||||
|
_fallbackCommands = [_openUrlFallbackItem, _fallbackItem];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IFallbackCommandItem[]? FallbackCommands() => [_openUrlFallbackItem, _fallbackItem];
|
public override ICommandItem[] TopLevelCommands() => _topLevelItems;
|
||||||
|
|
||||||
|
public override IFallbackCommandItem[]? FallbackCommands() => _fallbackCommands;
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_webSearchTopLevelItem?.Dispose();
|
||||||
|
|
||||||
|
base.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||||
@@ -13,7 +12,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.Ext.WebSearch;
|
namespace Microsoft.CmdPal.Ext.WebSearch;
|
||||||
|
|
||||||
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler
|
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
|
||||||
{
|
{
|
||||||
private readonly SettingsManager _settingsManager;
|
private readonly SettingsManager _settingsManager;
|
||||||
|
|
||||||
@@ -27,17 +26,29 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
|
|||||||
|
|
||||||
private void SetDefaultTitle() => Title = Resources.command_item_title;
|
private void SetDefaultTitle() => Title = Resources.command_item_title;
|
||||||
|
|
||||||
|
private void ReplaceCommand(ICommand newCommand)
|
||||||
|
{
|
||||||
|
(Command as IDisposable)?.Dispose();
|
||||||
|
Command = newCommand;
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateQuery(string query)
|
public void UpdateQuery(string query)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(query))
|
if (string.IsNullOrEmpty(query))
|
||||||
{
|
{
|
||||||
SetDefaultTitle();
|
SetDefaultTitle();
|
||||||
Command = new WebSearchListPage(_settingsManager);
|
ReplaceCommand(new WebSearchListPage(_settingsManager));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Title = query;
|
Title = query;
|
||||||
Command = new SearchWebCommand(query, _settingsManager);
|
ReplaceCommand(new SearchWebCommand(query, _settingsManager));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
(Command as IDisposable)?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
|||||||
|
|
||||||
if (_results is not null && _results.Count != 0)
|
if (_results is not null && _results.Count != 0)
|
||||||
{
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
var count = _results.Count;
|
var count = _results.Count;
|
||||||
var results = new ListItem[count];
|
var results = new ListItem[count];
|
||||||
var next = 0;
|
var next = 0;
|
||||||
@@ -82,6 +83,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
Logger.LogDebug($"Building ListItems took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(GetItems));
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -244,15 +247,22 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
|||||||
|
|
||||||
// foreach (var catalog in connections)
|
// foreach (var catalog in connections)
|
||||||
{
|
{
|
||||||
|
Stopwatch findPackages_stopwatch = new();
|
||||||
|
findPackages_stopwatch.Start();
|
||||||
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
|
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Logger.LogDebug($"Preface for \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
|
||||||
|
|
||||||
// BODGY, re: microsoft/winget-cli#5151
|
// BODGY, re: microsoft/winget-cli#5151
|
||||||
// FindPackagesAsync isn't actually async.
|
// FindPackagesAsync isn't actually async.
|
||||||
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
|
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
|
||||||
var searchResults = await internalSearchTask;
|
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:
|
// TODO more error handling like this:
|
||||||
if (searchResults.Status != FindPackagesResultStatus.Ok)
|
if (searchResults.Status != FindPackagesResultStatus.Ok)
|
||||||
{
|
{
|
||||||
@@ -261,6 +271,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
|
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
|
||||||
|
|
||||||
// FYI Using .ToArray or any other kind of enumerable loop
|
// FYI Using .ToArray or any other kind of enumerable loop
|
||||||
|
|||||||
@@ -1,39 +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.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Resources;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.CmdPal.Ext.WindowsSettings.Classes;
|
|
||||||
using Microsoft.CmdPal.Ext.WindowsSettings.Properties;
|
|
||||||
using Microsoft.CommandPalette.Extensions;
|
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
|
||||||
using Windows.Networking.NetworkOperators;
|
|
||||||
using Windows.UI;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Ext.WindowsSettings.Commands;
|
|
||||||
|
|
||||||
internal sealed partial class CopySettingCommand : InvokableCommand
|
|
||||||
{
|
|
||||||
private readonly WindowsSetting _entry;
|
|
||||||
|
|
||||||
internal CopySettingCommand(WindowsSetting entry)
|
|
||||||
{
|
|
||||||
Name = Resources.CopyCommand;
|
|
||||||
Icon = Icons.CopyIcon;
|
|
||||||
_entry = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CommandResult Invoke()
|
|
||||||
{
|
|
||||||
ClipboardHelper.SetText(_entry.Command);
|
|
||||||
|
|
||||||
return CommandResult.Dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@ internal static class ContextMenuHelper
|
|||||||
{
|
{
|
||||||
var list = new List<CommandContextItem>(1)
|
var list = new List<CommandContextItem>(1)
|
||||||
{
|
{
|
||||||
new(new CopySettingCommand(entry)),
|
new(new CopyTextCommand(entry.Command) { Name = Resources.CopyCommand }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
namespace SamplePagesExtension.Pages;
|
||||||
|
|
||||||
|
internal sealed partial class SampleIconPage : ListPage
|
||||||
|
{
|
||||||
|
private readonly IListItem[] _items =
|
||||||
|
[
|
||||||
|
/*
|
||||||
|
* Quick intro to Unicode in source code:
|
||||||
|
* - Every character has a code point (e.g., U+0041 = 'A').
|
||||||
|
* - Code points up to U+FFFF use \u1234 (4 hex digits and lowercase u).
|
||||||
|
* - Code points above that (up to U+10FFFF) use \U12345678 (8 hex digits and capital letter U).
|
||||||
|
* - If your source file is UTF-8, you can type the character directly, but it may not display properly in editors,
|
||||||
|
* and it's harder to see the actual code point.
|
||||||
|
* - Some symbols (like many emojis) are built from multiple code points
|
||||||
|
* joined together (e.g., 👋🏻 = U+1F44B + U+1F3FB).
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* 😍 = "😍" or "\U0001F60D"
|
||||||
|
* 👋🏻 = "👋🏻" or "\U0001F44B\U0001F3FB"
|
||||||
|
* 🧙♂️ = "🧙♂️" or "\U0001F9D9\u200D\u2642\U0000FE0F" (male mage)
|
||||||
|
* 🧙🏿♀️ = "🧙🏿♀️" or "\U0001F9D9\U0001F3FF\u200D\u2640\U0000FE0F" (dark-skinned woman mage)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Emoji Smiling Face with Heart-Eyes
|
||||||
|
// Unicode: \U0001F60D
|
||||||
|
BuildIconItem("😍", "Standard emoji icon", "Basic emoji character rendered as an icon"),
|
||||||
|
|
||||||
|
// Emoji Smiling Face with Heart-Eyes
|
||||||
|
// Unicode: \U0001F60D\U0001F643\U0001F622
|
||||||
|
BuildIconItem("😍🙃😢", "Multiple emojis", "Use of multiple emojis for icon is not allowed"),
|
||||||
|
|
||||||
|
// Emoji Smiling Face with Sunglasses
|
||||||
|
// Unicode: \U0001F60E
|
||||||
|
BuildIconItem("\U0001F60E", "Unicode escape sequence emoji", "Emoji defined using Unicode escape sequence notation"),
|
||||||
|
|
||||||
|
// Segoe Fluent Icons font icon
|
||||||
|
// Unicode: \uE8D4
|
||||||
|
BuildIconItem("\uE8D4", "Segoe Fluent icon demonstration", "Segoe Fluent/MDL2 icon from system font\nWorks as an icon but won't display properly in button text"),
|
||||||
|
|
||||||
|
// Extended pictographic symbol for keyboard
|
||||||
|
BuildIconItem("\u2328", "Extended pictographic symbol", "Pictographic symbol representing a keyboard"),
|
||||||
|
|
||||||
|
// Capital letter A
|
||||||
|
BuildIconItem("A", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||||
|
|
||||||
|
// Letter 1
|
||||||
|
// Unicode: \U00000031
|
||||||
|
BuildIconItem("1", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||||
|
|
||||||
|
// Emoji Keycap Digit Two ... 2️⃣
|
||||||
|
// Unicode: \U00000032\U000020E3
|
||||||
|
// This is a sequence of three code points: the digit '2' (U+0032), and a combining enclosing keycap (U+20E3). No variation selector is used here.
|
||||||
|
BuildIconItem("\U00000032\U000020E3", "Emoji without variation selector", "Emoji character doesn't have VS16 variation selector to render as text"),
|
||||||
|
|
||||||
|
// Emoji Keycap Digit Three ... 3️⃣
|
||||||
|
// Unicode: \U00000033\U0000FE0F\U000020E3
|
||||||
|
// This is a sequence of three code points: the digit '3' (U+0033), a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
|
||||||
|
BuildIconItem("3️⃣", "Emoji with variation selector", "Emoji character using a variation selector to specify emoji presentation"),
|
||||||
|
|
||||||
|
// Symbol #
|
||||||
|
// Unicode: \u0023
|
||||||
|
BuildIconItem("#", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||||
|
|
||||||
|
// Symbol # keycap
|
||||||
|
// Unicode: \u0023\ufe0f\u20e3
|
||||||
|
// Sequence of 3 code points: symbol #, a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
|
||||||
|
BuildIconItem("\u0023\ufe0f\u20e3", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||||
|
|
||||||
|
// Capital letter WM
|
||||||
|
// This is two characters, which is not a valid icon representation. It will be replaced by a placeholder signalizing an invalid icon.
|
||||||
|
BuildIconItem("WM", "Invalid icon representation", "String with multiple characters that does not correspond to a valid single icon"),
|
||||||
|
|
||||||
|
// Emoji Mage
|
||||||
|
// Unicode: \U0001F9D9
|
||||||
|
BuildIconItem("🧙", "Single code-point emoji example", "Simple emoji character using a single Unicode code point"),
|
||||||
|
|
||||||
|
// Emoji Male Mage (Mage with gender modifier)
|
||||||
|
// Unicode: \U0001F9D9\u200D\u2642\uFE0F
|
||||||
|
BuildIconItem("🧙♂️", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for male variant"),
|
||||||
|
|
||||||
|
// Emoji Woman Mage (Mage with gender modifier)
|
||||||
|
// Unicode: \U0001F9D9\u200D\u2640\uFE0F
|
||||||
|
BuildIconItem("\U0001F9D9\u200D\u2640\uFE0F", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for female variant"),
|
||||||
|
|
||||||
|
// Emoji Waving Hand
|
||||||
|
// Unicode: \U0001F44B
|
||||||
|
BuildIconItem("👋", "Basic hand gesture emoji", "Standard emoji character representing a waving hand"),
|
||||||
|
|
||||||
|
// Emoji Waving Hand + Light Skin Tone
|
||||||
|
// Unicode: \U0001F44B\U0001F3FB
|
||||||
|
BuildIconItem("👋🏻", "Emoji with light skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (light)"),
|
||||||
|
|
||||||
|
// Emoji Waving Hand + Dark Skin Tone
|
||||||
|
// Unicode: \U0001F44B\U0001F3FF
|
||||||
|
BuildIconItem("\U0001F44B\U0001F3FF", "Emoji with dark skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (dark)"),
|
||||||
|
|
||||||
|
// Flag of Czechia (Czech Republic)
|
||||||
|
// Unicode: \U0001F1E8\U0001F1FF
|
||||||
|
BuildIconItem("\U0001F1E8\U0001F1FF", "Flag emoji using regional indicators", "Emoji flag constructed from regional indicator symbols for Czechia"),
|
||||||
|
|
||||||
|
// Use of ZWJ without emojis
|
||||||
|
// KA (\u0995) + VIRAMA (\u09CD) + ZWJ (\u200D) - shows the half-form KA
|
||||||
|
// Unicode: \u0995\u09CD\u200D
|
||||||
|
BuildIconItem("\u0995\u09CD\u200D", "Use of ZWJ in non-emoji context", "Shows the half-form KA"),
|
||||||
|
|
||||||
|
// Use of ZWJ without emojis
|
||||||
|
// KA (\u0995) + VIRAMA (\u09CD) + Shows full KA with an explicit virama mark (not half-form).
|
||||||
|
// Unicode: \u0995\u09CD
|
||||||
|
BuildIconItem("\u0995\u09CD", "Use of ZWJ in non-emoji context", "Shows full KA with an explicit virama mark"),
|
||||||
|
|
||||||
|
// mahjong tile red dragon (using Unicode escape sequence)
|
||||||
|
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
|
||||||
|
// Unicode: \U0001F004
|
||||||
|
BuildIconItem("\U0001F004", "Mahjong tile emoji (red dragon)", "Mahjong tile red dragon emoji character using Unicode escape sequence"),
|
||||||
|
|
||||||
|
// mahjong tile green dragon (non-emoji)
|
||||||
|
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
|
||||||
|
// Unicode: \U0001F005
|
||||||
|
BuildIconItem("\U0001F005", "Mahjong tile non-emoji (green dragon)", "Mahjong tile character that is not classified as an emoji"),
|
||||||
|
|
||||||
|
// Play, PlayPause, Stop
|
||||||
|
BuildIconItem("\u25B6", "Play symbol (standalone)", "Play symbol"),
|
||||||
|
BuildIconItem("\u25B6\uFE0E", "Play symbol + VS15 (request text)", "Play symbol with variation specifier requesting rendering as text"),
|
||||||
|
BuildIconItem("\u25B6\uFE0F", "Play symbol + VS16 (request emoji)", "Play symbol with variation specifier requesting rendering as emoji "),
|
||||||
|
BuildIconItem("⏯️", "Play/Pause keycap emoji", "Play/Pause keycap emoji doesn't have plain text variant"),
|
||||||
|
BuildIconItem("⏸️", "Pause keycap emoji", "Pause keycap emoji doesn't have plain text variant"),
|
||||||
|
|
||||||
|
// Copyright and emoji copyright:
|
||||||
|
BuildIconItem("\u00a9", "Copyright symbol (standalone)", "Copyright symbol that is not classified as an emoji"),
|
||||||
|
BuildIconItem("\u00a9\uFE0E", "Copyright symbol + VS15 (request text)", "Copyright symbol that is not classified as an emoji"),
|
||||||
|
BuildIconItem("\u00a9\uFE0F", "Copyright symbol + VS16 (request emoji)", "Copyright symbol that is not classified as an emoji"),
|
||||||
|
|
||||||
|
// Tag flags
|
||||||
|
BuildIconItem("🏳️", "White Flag", "White Flag"),
|
||||||
|
BuildIconItem("\U0001F3F4\u200D\u2620\uFE0F", "Pirate Flag", "Pirate Flag"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public SampleIconPage()
|
||||||
|
{
|
||||||
|
Icon = new IconInfo("\uE8BA");
|
||||||
|
Name = "Sample Icon Page";
|
||||||
|
ShowDetails = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IListItem[] GetItems() => _items;
|
||||||
|
|
||||||
|
private static ListItem BuildIconItem(string icon, string title, string description)
|
||||||
|
{
|
||||||
|
var iconInfo = new IconInfo(icon);
|
||||||
|
|
||||||
|
return new ListItem(new CopyTextCommand(icon) { Name = "Action with " + icon })
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Subtitle = description,
|
||||||
|
Icon = iconInfo,
|
||||||
|
Tags = [
|
||||||
|
new Tag("Tag") { Icon = iconInfo },
|
||||||
|
],
|
||||||
|
Details = new Details
|
||||||
|
{
|
||||||
|
HeroImage = iconInfo,
|
||||||
|
Title = title,
|
||||||
|
Body = description,
|
||||||
|
Metadata = [
|
||||||
|
new DetailsElement
|
||||||
|
{
|
||||||
|
Key = "Unicode Code Points",
|
||||||
|
Data = new DetailsTags
|
||||||
|
{
|
||||||
|
Tags = icon.EnumerateRunes()
|
||||||
|
.Select(rune => rune.Value <= 0xFFFF ? $"\\u{rune.Value:X4}" : $"\\U{rune.Value:X8}")
|
||||||
|
.Select(t => new Tag(t))
|
||||||
|
.ToArray<ITag>(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using SamplePagesExtension.Pages;
|
||||||
|
|
||||||
namespace SamplePagesExtension;
|
namespace SamplePagesExtension;
|
||||||
|
|
||||||
@@ -37,6 +38,11 @@ public partial class SamplesListPage : ListPage
|
|||||||
Title = "Demo of OnLoad/OnUnload",
|
Title = "Demo of OnLoad/OnUnload",
|
||||||
Subtitle = "Changes the list of items every time the page is opened / closed",
|
Subtitle = "Changes the list of items every time the page is opened / closed",
|
||||||
},
|
},
|
||||||
|
new ListItem(new SampleIconPage())
|
||||||
|
{
|
||||||
|
Title = "Sample Icon Page",
|
||||||
|
Subtitle = "A demo of using icons in various ways",
|
||||||
|
},
|
||||||
|
|
||||||
// Content pages
|
// Content pages
|
||||||
new ListItem(new SampleContentPage())
|
new ListItem(new SampleContentPage())
|
||||||
|
|||||||
@@ -43,13 +43,18 @@ public partial class ListHelpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
|
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
|
var scores = items
|
||||||
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })
|
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })
|
||||||
.Where(score => score.Score > 0)
|
.Where(score => score.Score > 0)
|
||||||
.OrderByDescending(score => score.Score);
|
.OrderByDescending(score => score.Score);
|
||||||
return scores
|
return scores;
|
||||||
.Select(score => score.Item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -372,20 +372,17 @@ namespace UITests_FancyZones
|
|||||||
// launch FancyZones settings page
|
// launch FancyZones settings page
|
||||||
private void LaunchFancyZones()
|
private void LaunchFancyZones()
|
||||||
{
|
{
|
||||||
if (this.FindAll<NavigationViewItem>("FancyZones").Count == 0)
|
this.Find<NavigationViewItem>(By.AccessibilityId("WindowingAndLayoutsNavItem")).Click();
|
||||||
{
|
|
||||||
this.Find<NavigationViewItem>("Windowing & Layouts").Click();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Find<NavigationViewItem>("FancyZones").Click();
|
this.Find<NavigationViewItem>(By.AccessibilityId("FancyZonesNavItem")).Click();
|
||||||
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true);
|
this.Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
|
||||||
|
|
||||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||||
Find<Element>(By.AccessibilityId("HeaderPresenter")).Click();
|
Find<Element>(By.AccessibilityId("HeaderPresenter")).Click();
|
||||||
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
|
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
|
||||||
ZoneBehaviourSettings(TestContext.TestName);
|
ZoneBehaviourSettings(TestContext.TestName);
|
||||||
|
|
||||||
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 500, 10000);
|
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000);
|
||||||
this.Session.Attach(PowerToysModule.FancyZone);
|
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.
|
// pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required.
|
||||||
@@ -403,7 +400,7 @@ namespace UITests_FancyZones
|
|||||||
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
|
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
|
||||||
this.Session.Attach(PowerToysModule.PowerToysSettings);
|
this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||||
SetupCustomLayouts();
|
SetupCustomLayouts();
|
||||||
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 5000, 5000);
|
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 5000, 5000);
|
||||||
this.Session.Attach(PowerToysModule.FancyZone);
|
this.Session.Attach(PowerToysModule.FancyZone);
|
||||||
this.Find<Microsoft.PowerToys.UITest.Button>("Maximize").Click();
|
this.Find<Microsoft.PowerToys.UITest.Button>("Maximize").Click();
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,32 @@ private:
|
|||||||
//contains the non localized key of the powertoy
|
//contains the non localized key of the powertoy
|
||||||
std::wstring app_key;
|
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:
|
public:
|
||||||
// Constructor
|
// Constructor
|
||||||
ImageResizerModule()
|
ImageResizerModule()
|
||||||
{
|
{
|
||||||
m_enabled = CSettingsInstance().GetEnabled();
|
m_enabled = CSettingsInstance().GetEnabled();
|
||||||
|
UpdateRegistration(m_enabled);
|
||||||
app_name = GET_RESOURCE_STRING(IDS_IMAGERESIZER);
|
app_name = GET_RESOURCE_STRING(IDS_IMAGERESIZER);
|
||||||
app_key = ImageResizerConstants::ModuleKey;
|
app_key = ImageResizerConstants::ModuleKey;
|
||||||
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::imageResizerLoggerName);
|
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::imageResizerLoggerName);
|
||||||
@@ -112,10 +133,7 @@ public:
|
|||||||
package::RegisterSparsePackage(path, packageUri);
|
package::RegisterSparsePackage(path, packageUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
UpdateRegistration(m_enabled);
|
||||||
ImageResizerRuntimeRegistration::EnsureRegistered();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Trace::EnableImageResizer(m_enabled);
|
Trace::EnableImageResizer(m_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +141,8 @@ public:
|
|||||||
virtual void disable()
|
virtual void disable()
|
||||||
{
|
{
|
||||||
m_enabled = false;
|
m_enabled = false;
|
||||||
|
UpdateRegistration(m_enabled);
|
||||||
Trace::EnableImageResizer(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
|
// Returns if the powertoys is enabled
|
||||||
|
|||||||
@@ -168,6 +168,25 @@ private:
|
|||||||
//contains the non localized key of the powertoy
|
//contains the non localized key of the powertoy
|
||||||
std::wstring app_key;
|
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:
|
public:
|
||||||
// Return the localized display name of the powertoy
|
// Return the localized display name of the powertoy
|
||||||
virtual PCWSTR get_name() override
|
virtual PCWSTR get_name() override
|
||||||
@@ -202,9 +221,7 @@ public:
|
|||||||
package::RegisterSparsePackage(path, packageUri);
|
package::RegisterSparsePackage(path, packageUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
UpdateRegistration(m_enabled);
|
||||||
PowerRenameRuntimeRegistration::EnsureRegistered();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable the powertoy
|
// Disable the powertoy
|
||||||
@@ -212,10 +229,7 @@ public:
|
|||||||
{
|
{
|
||||||
m_enabled = false;
|
m_enabled = false;
|
||||||
Logger::info(L"PowerRename disabled");
|
Logger::info(L"PowerRename disabled");
|
||||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
UpdateRegistration(m_enabled);
|
||||||
PowerRenameRuntimeRegistration::Unregister();
|
|
||||||
Logger::info(L"PowerRename context menu unregistered (Win10)");
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns if the powertoy is enabled
|
// Returns if the powertoy is enabled
|
||||||
@@ -315,6 +329,7 @@ public:
|
|||||||
void init_settings()
|
void init_settings()
|
||||||
{
|
{
|
||||||
m_enabled = CSettingsInstance().GetEnabled();
|
m_enabled = CSettingsInstance().GetEnabled();
|
||||||
|
UpdateRegistration(m_enabled);
|
||||||
Trace::EnablePowerRename(m_enabled);
|
Trace::EnablePowerRename(m_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ namespace RegistryPreview
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void AppWindow_Closing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args)
|
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.X", JsonValue.CreateNumberValue(AppWindow.Position.X));
|
||||||
jsonWindowPlacement.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(appWindow.Position.Y));
|
jsonWindowPlacement.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(AppWindow.Position.Y));
|
||||||
jsonWindowPlacement.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(appWindow.Size.Width));
|
jsonWindowPlacement.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(AppWindow.Size.Width));
|
||||||
jsonWindowPlacement.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(appWindow.Size.Height));
|
jsonWindowPlacement.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(AppWindow.Size.Height));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user