mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-12 14:20:17 +01:00
Compare commits
24 Commits
dev/vanzue
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fdbab1d2f | ||
|
|
3036a944c5 | ||
|
|
9e7060ab68 | ||
|
|
45ce27c115 | ||
|
|
2ec439fb27 | ||
|
|
a46a67c3aa | ||
|
|
6ef3670321 | ||
|
|
b93a496bcd | ||
|
|
a15a7587cc | ||
|
|
603ac55f8a | ||
|
|
3f5418132d | ||
|
|
e935faf08c | ||
|
|
eb5f4c6bd0 | ||
|
|
658f90d6f8 | ||
|
|
3f3e04086e | ||
|
|
3b874a9567 | ||
|
|
7a86543c8d | ||
|
|
67a013f729 | ||
|
|
521d34f1eb | ||
|
|
a02a5a9736 | ||
|
|
1e25d17920 | ||
|
|
4959273875 | ||
|
|
095961402b | ||
|
|
7477b561a1 |
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.Windows.Settings/WindowsSettings
|
||||
@@ -13,11 +13,11 @@ properties:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2022 Enterprise (Any edition will work)
|
||||
description: Install Visual Studio 2026 Enterprise (Any edition will work)
|
||||
# Requires elevation for the set operation
|
||||
securityContext: elevated
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.2022.Enterprise
|
||||
id: Microsoft.VisualStudio.Enterprise
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
securityContext: elevated
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.Enterprise
|
||||
channelId: VisualStudio.17.Release
|
||||
channelId: VisualStudio.18.Release
|
||||
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.Windows.Settings/WindowsSettings
|
||||
@@ -13,11 +13,11 @@ properties:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2022 Professional (Any edition will work)
|
||||
description: Install Visual Studio 2026 Professional (Any edition will work)
|
||||
# Requires elevation for the set operation
|
||||
securityContext: elevated
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.2022.Professional
|
||||
id: Microsoft.VisualStudio.Professional
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
securityContext: elevated
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.Professional
|
||||
channelId: VisualStudio.17.Release
|
||||
channelId: VisualStudio.18.Release
|
||||
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.Windows.Settings/WindowsSettings
|
||||
@@ -13,11 +13,11 @@ properties:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2022 Community (Any edition will work)
|
||||
description: Install Visual Studio 2026 Community (Any edition will work)
|
||||
# Requires elevation for the set operation
|
||||
securityContext: elevated
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.2022.Community
|
||||
id: Microsoft.VisualStudio.Community
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
securityContext: elevated
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.Community
|
||||
channelId: VisualStudio.17.Release
|
||||
channelId: VisualStudio.18.Release
|
||||
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
|
||||
7
.github/actions/spell-check/allow/code.txt
vendored
7
.github/actions/spell-check/allow/code.txt
vendored
@@ -38,6 +38,7 @@ Gbps
|
||||
gcode
|
||||
Heatshrink
|
||||
Mbits
|
||||
Kbits
|
||||
MBs
|
||||
mkv
|
||||
msix
|
||||
@@ -97,6 +98,7 @@ Yubico
|
||||
Perplexity
|
||||
Groq
|
||||
svgl
|
||||
devhome
|
||||
|
||||
# KEYS
|
||||
|
||||
@@ -322,6 +324,7 @@ REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
@@ -342,3 +345,7 @@ reportbug
|
||||
#ffmpeg
|
||||
crf
|
||||
nostdin
|
||||
|
||||
# Performance counter keys
|
||||
engtype
|
||||
Nonpaged
|
||||
|
||||
3
.github/actions/spell-check/allow/names.txt
vendored
3
.github/actions/spell-check/allow/names.txt
vendored
@@ -192,6 +192,7 @@ ycv
|
||||
yeelam
|
||||
Yuniardi
|
||||
yuyoyuppe
|
||||
zadjii
|
||||
Zeol
|
||||
Zhao
|
||||
Zhaopeng
|
||||
@@ -228,6 +229,7 @@ regedit
|
||||
roslyn
|
||||
Skia
|
||||
Spotify
|
||||
taskmgr
|
||||
tldr
|
||||
Vanara
|
||||
wangyi
|
||||
@@ -243,4 +245,3 @@ xamlstyler
|
||||
Xavalon
|
||||
Xbox
|
||||
Youdao
|
||||
zadjii
|
||||
|
||||
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -111,6 +111,7 @@
|
||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/Text/.*\.cs$
|
||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||
|
||||
4
.github/actions/spell-check/expect.txt
vendored
4
.github/actions/spell-check/expect.txt
vendored
@@ -197,6 +197,7 @@ Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
carlos
|
||||
Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
@@ -217,6 +218,7 @@ certmgr
|
||||
cfp
|
||||
CHANGECBCHAIN
|
||||
changecursor
|
||||
chatasweetie
|
||||
checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
@@ -1527,6 +1529,7 @@ randi
|
||||
RAquadrant
|
||||
rasterization
|
||||
Rasterize
|
||||
rasterizing
|
||||
RAWINPUTDEVICE
|
||||
RAWINPUTHEADER
|
||||
RAWMODE
|
||||
@@ -2241,6 +2244,7 @@ YSpeed
|
||||
YStr
|
||||
YTimer
|
||||
YVIRTUALSCREEN
|
||||
zamora
|
||||
ZEROINIT
|
||||
zonability
|
||||
zonable
|
||||
|
||||
@@ -219,6 +219,10 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
30
README.md
30
README.md
@@ -103,10 +103,38 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.97.1 (January 2026)**
|
||||
|
||||
**Version 0.97.2 (Feb 2026)**
|
||||
|
||||
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
|
||||
|
||||
## Advanced Paste
|
||||
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
|
||||
|
||||
## Color Picker
|
||||
- #45367 Fixed contrast issue in Color picker UI.
|
||||
|
||||
## Command Palette
|
||||
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
|
||||
|
||||
## Cursor Wrap
|
||||
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
|
||||
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
## Image Resizer
|
||||
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
|
||||
|
||||
## LightSwitch
|
||||
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
|
||||
|
||||
## Workspaces
|
||||
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
|
||||
|
||||
## Quick Access and Measure Tool
|
||||
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
|
||||
|
||||
**Version 0.97.1 (January 2026)**
|
||||
|
||||
**Highlights**
|
||||
|
||||
### Advanced Paste
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
### Building PowerToys Locally
|
||||
|
||||
#### One stop script for building installer
|
||||
1. Open `Developer Powershell for VS 2022` or `Developer PowerShell for VS` for VS 2026.
|
||||
1. Open `Developer PowerShell for VS`.
|
||||
2. Run tools\build\build-installer.ps1
|
||||
> For the first-time setup, please run the installer as an administrator. This ensures that the Wix tool can move wix.target to the desired location and trust the certificate used to sign the MSIX packages.
|
||||
|
||||
@@ -109,7 +109,7 @@ dotnet tool install --global wix --version 5.0.2
|
||||
|
||||
##### From the command line
|
||||
|
||||
1. From the start menu, open a `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS`
|
||||
1. From the start menu, open a `Developer Command Prompt for VS`
|
||||
1. Ensure `nuget.exe` is in your `%path%`
|
||||
1. In the repo root, run these commands:
|
||||
|
||||
@@ -140,7 +140,7 @@ If you prefer, you can alternatively build prerequisite projects for the install
|
||||
|
||||
The resulting installer will be available in the `installer\PowerToysSetupVNext\x64\Release\` folder.
|
||||
|
||||
To build the installer from the command line, run `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS` in admin mode and execute the following commands. The generated installer package will be located at `\installer\PowerToysSetupVNext\{platform}\Release\MachineSetup`.
|
||||
To build the installer from the command line, run `Developer Command Prompt for VS` in admin mode and execute the following commands. The generated installer package will be located at `\installer\PowerToysSetupVNext\{platform}\Release\MachineSetup`.
|
||||
|
||||
```
|
||||
git clean -xfd -e *exe -- .\installer\
|
||||
|
||||
@@ -15,7 +15,7 @@ Before you can start debugging PowerToys, you need to set up your development en
|
||||
|
||||
You can build the entire solution from the command line, which is sometimes faster than building within Visual Studio:
|
||||
|
||||
1. Open `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS`
|
||||
1. Open `Developer Command Prompt for VS`
|
||||
2. Navigate to the repository root directory
|
||||
3. Run the following command(don't forget to set the correct platform):
|
||||
```pwsh
|
||||
@@ -105,7 +105,7 @@ If you encounter build errors about missing image files (e.g., `.png`, `.ico`, o
|
||||
|
||||
1. **Clean the solution in Visual Studio**: Build > Clean Solution
|
||||
|
||||
Or from the command line (Developer Command Prompt for VS 2022 or Developer Command Prompt for VS):
|
||||
Or from the command line (`Developer Command Prompt for VS`):
|
||||
```pwsh
|
||||
msbuild PowerToys.slnx /t:Clean /p:Platform=x64 /p:Configuration=Debug
|
||||
```
|
||||
|
||||
@@ -15,9 +15,27 @@ VS Code extensions Needed:
|
||||
---
|
||||
|
||||
## Building in VS Code
|
||||
### Configure Developer Powershell for VS 2022 or Developer Powershell for VS for more convenient dev in vscode.
|
||||
1. Configure profile in in settings, entry: "terminal.integrated.profiles.windows"
|
||||
2. Add below config as entry (choose VS 2022 or VS 2026 based on your installation):
|
||||
### Configure Developer PowerShell for VS for more convenient development experience in VS Code
|
||||
1. Configure profile in settings, entry: `terminal.integrated.profiles.windows`
|
||||
2. Add below config as entry (choose VS 2026 or VS 2022 based on your installation):
|
||||
|
||||
**For Visual Studio 2026 (recommended):**
|
||||
```json
|
||||
"Developer PowerShell for VS": {
|
||||
// Configure based on your preference
|
||||
"path": "C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.5.2.0_arm64__8wekyb3d8bbwe\\pwsh.exe",
|
||||
"args": [
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"& {",
|
||||
"$orig = Get-Location;",
|
||||
// Adjust path based on your edition (Community/Professional/Enterprise)
|
||||
"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
|
||||
"Set-Location $orig",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
**For Visual Studio 2022:**
|
||||
```json
|
||||
@@ -37,24 +55,6 @@ VS Code extensions Needed:
|
||||
},
|
||||
```
|
||||
|
||||
**For Visual Studio 2026:**
|
||||
```json
|
||||
"Developer PowerShell for VS": {
|
||||
// Configure based on your preference
|
||||
"path": "C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.5.2.0_arm64__8wekyb3d8bbwe\\pwsh.exe",
|
||||
"args": [
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"& {",
|
||||
"$orig = Get-Location;",
|
||||
// Adjust path based on your edition (Community/Professional/Enterprise)
|
||||
"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
|
||||
"Set-Location $orig",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
3. [Optional] Set your Developer PowerShell profile as the default, so that you can get a deep integration with vscode coding agent.
|
||||
|
||||
4. Now you can build with plain `msbuild` or configure tasks.json in below section.
|
||||
|
||||
@@ -12,22 +12,11 @@ A PowerToy module is a self-contained utility integrated into the PowerToys ecos
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) and the following workloads/individual components:
|
||||
- Desktop Development with C++
|
||||
- WinUI application development
|
||||
- .NET desktop development
|
||||
- Windows 10 SDK (10.0.22621.0)
|
||||
- Windows 11 SDK (10.0.26100.3916)
|
||||
- .NET 8 SDK
|
||||
- Fork the [PowerToys repository](https://github.com/microsoft/PowerToys/tree/main) locally
|
||||
- [Validate that you are able to build and run](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md) `PowerToys.slnx`.
|
||||
Follow the [Getting Started](../readme.md#getting-started) guide to set up your development environment, then [validate that you are able to build and run](debugging.md) `PowerToys.slnx`.
|
||||
|
||||
Optional:
|
||||
- [WiX v5 toolset](https://github.com/microsoft/PowerToys/tree/main) for the installer
|
||||
|
||||
> [!NOTE]
|
||||
> To ensure all the correct VS Workloads are installed, use [the WinGet configuration files](https://github.com/microsoft/PowerToys/tree/e13d6a78aafbcf32a4bb5f8581d041e1d057c3f1/.config) in the project repository. (Use the one that matches your VS distribution. ie: VS Community would use `configuration.winget`)
|
||||
|
||||
### Folder structure
|
||||
|
||||
```
|
||||
|
||||
197
doc/devdocs/events.md
Normal file
197
doc/devdocs/events.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Telemetry Events
|
||||
|
||||
PowerToys collects limited telemetry to understand feature usage, reliability, and product quality. When adding a new telemetry event, follow the steps below to ensure the event is properly declared, documented, and available after release.
|
||||
|
||||
**⚠️ Important**: Telemetry must never include personal information, file paths, or user‑generated content.
|
||||
|
||||
## Developer Effort Overview (What to Expect)
|
||||
|
||||
Adding a telemetry event is a **multi-step process** that typically spans several areas of the codebase and documentation.
|
||||
|
||||
At a high level, developers should expect to:
|
||||
|
||||
1. Within one PR:
|
||||
1. Add a new telemetry event(s) to module
|
||||
1. Add the new event(s) DATA_AND_PRIVACY.md
|
||||
1. Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s)
|
||||
|
||||
### Privacy Guidelines
|
||||
|
||||
**NEVER** log:
|
||||
|
||||
- User data (text, files, emails, etc.)
|
||||
- File paths or filenames
|
||||
- Personal information
|
||||
- Sensitive system information
|
||||
- Anything that could identify a specific user
|
||||
|
||||
DO log:
|
||||
|
||||
- Feature usage (which features, how often)
|
||||
- Success/failure status
|
||||
- Timing/performance metrics
|
||||
- Error types (not error messages with user data)
|
||||
- Aggregate counts
|
||||
|
||||
### Event Naming Convention
|
||||
|
||||
Follow this pattern: `UtilityName_EventDescription`
|
||||
|
||||
Examples:
|
||||
|
||||
- `ColorPicker_Session`
|
||||
- `FancyZones_LayoutApplied`
|
||||
- `PowerRename_Rename`
|
||||
- `AdvancedPaste_FormatClicked`
|
||||
- `CmdPal_ExtensionInvoked`
|
||||
|
||||
## Adding Telemetry Events to PowerToys
|
||||
|
||||
PowerToys uses ETW (Event Tracing for Windows) for telemetry in both C++ and C# modules. The telemetry system is:
|
||||
|
||||
- Opt-in by default (disabled since v0.86)
|
||||
- Privacy-focused - never logs personal info, file paths, or user-generated content
|
||||
- Controlled by registry - HKEY_CURRENT_USER\Software\Classes\PowerToys\AllowDataDiagnostics
|
||||
|
||||
### C++ Telemetry Implementation
|
||||
|
||||
**Core Components**
|
||||
|
||||
| File | Purpose |
|
||||
| ------------- |:-------------:|
|
||||
| [ProjectTelemetry.h](../../src/common/Telemetry/ProjectTelemetry.h) | Declares the global ETW provider g_hProvider |
|
||||
| [TraceBase.h](../../src/common/Telemetry/TraceBase.h) | Base class with RegisterProvider(), UnregisterProvider(), and IsDataDiagnosticsEnabled() check |
|
||||
| [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Privacy tags and telemetry option group macros
|
||||
|
||||
|
||||
#### Pattern for C++ Modules
|
||||
|
||||
1. Create a `Trace` class inheriting from `telemetry::TraceBase` (src/common/Telemetry/TraceBase.h):
|
||||
|
||||
```c
|
||||
// trace.h
|
||||
#pragma once
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
class Trace : public telemetry::TraceBase
|
||||
{
|
||||
public:
|
||||
static void MyEvent(/* parameters */);
|
||||
};
|
||||
```
|
||||
|
||||
2. Implement events using `TraceLoggingWriteWrapper`:
|
||||
|
||||
```cpp
|
||||
// trace.cpp
|
||||
#include "trace.h"
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
TRACELOGGING_DEFINE_PROVIDER(
|
||||
g_hProvider,
|
||||
"Microsoft.PowerToys",
|
||||
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
|
||||
TraceLoggingOptionProjectTelemetry());
|
||||
|
||||
void Trace::MyEvent(bool enabled)
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"ModuleName_EventName", // Event name
|
||||
TraceLoggingBoolean(enabled, "Enabled"), // Event data
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
```
|
||||
|
||||
**Key C++ Telemetry Macros**
|
||||
|
||||
| Macro | Purpose |
|
||||
| ------------- |:-------------:|
|
||||
| `TraceLoggingWriteWrapper` [CustomAction.cpp](../../installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp) | Wraps `TraceLoggingWrite` with `IsDataDiagnosticsEnabled()` check |
|
||||
| `ProjectTelemetryPrivacyDataTag(tag)` [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Sets privacy classification |
|
||||
|
||||
### C# Telemetry Implementation
|
||||
|
||||
**Core Components**
|
||||
|
||||
| File | Purpose |
|
||||
| ------------- |:-------------:|
|
||||
| [PowerToysTelemetry.cs](../../src/common/ManagedTelemetry/Telemetry/PowerToysTelemetry.cs) | Singleton `Log` instance with `WriteEvent<T>()` method |
|
||||
| [EventBase.cs](../../src/common/ManagedTelemetry/Telemetry/Events/EventBase.cs) | Base class for all events (provides `EventName`, `Version`) |
|
||||
| [IEvent.cs](../../src/common/ManagedTelemetry/Telemetry/Events/IEvent.cs) | Interface requiring `PartA_PrivTags` property |
|
||||
| [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Inherits from `EventSource`, defines ETW constants |
|
||||
| [DataDiagnosticsSettings.cs](../../src/common/ManagedTelemetry/Telemetry/DataDiagnosticsSettings.cs) | Registry-based enable/disable check
|
||||
|
||||
#### Pattern for C# Modules
|
||||
|
||||
1. Create an event class inheriting from `EventBase` and implementing `IEvent`:
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
namespace MyModule.Telemetry
|
||||
{
|
||||
[EventData]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public class MyModuleEvent : EventBase, IEvent
|
||||
{
|
||||
// Event properties (logged as telemetry data)
|
||||
public string SomeProperty { get; set; }
|
||||
public int SomeValue { get; set; }
|
||||
|
||||
// Required: Privacy tag
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
// Optional: Set EventName in constructor (defaults to class name)
|
||||
public MyModuleEvent(string prop, int val)
|
||||
{
|
||||
EventName = "MyModule_EventName";
|
||||
SomeProperty = prop;
|
||||
SomeValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Log the event:
|
||||
|
||||
```csharp
|
||||
PowerToysTelemetry.Log.WriteEvent(new MyModuleEvent("value", 42));
|
||||
```
|
||||
|
||||
**Privacy Tags (C#)**
|
||||
|
||||
| Tag | Use Case |
|
||||
| ------------- |:-------------:|
|
||||
| `PartA_PrivTags.ProductAndServiceUsage` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Feature usage events
|
||||
| `PartA_PrivTags.ProductAndServicePerformance` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Performance/timing events
|
||||
|
||||
### Update DATA_AND_PRIVACY.md file
|
||||
|
||||
Add your new event(s) to [DATA_AND_PRIVACY.md](../../DATA_AND_PRIVACY.md).
|
||||
|
||||
## Launch Product Version Containing the new events
|
||||
|
||||
Events do not become active until they ship in a released PowerToys version. After your PRs are merged:
|
||||
|
||||
- The event will begin firing once users install the version that includes it
|
||||
- In order for PowerToys to process these events, you must complete the next section
|
||||
|
||||
## Next Steps
|
||||
|
||||
Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s).
|
||||
|
||||
## Summary
|
||||
|
||||
Required steps:
|
||||
|
||||
1. In one PR:
|
||||
- Add the event(s) in code
|
||||
- Document event(s) in DATA_AND_PRIVACY.md
|
||||
1. Ship the change in a PowerToys release
|
||||
1. Reach out for next steps
|
||||
@@ -152,7 +152,7 @@ FancyZones is divided into several projects:
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
- Visual Studio 2022 or 2026: Required for building and debugging
|
||||
- Visual Studio 2026 (or 2022 17.4+): Required for building and debugging
|
||||
- Windows 10 SDK: Ensure the latest version is installed
|
||||
- PowerToys Repository: Clone from GitHub
|
||||
|
||||
@@ -183,7 +183,7 @@ FancyZones is divided into several projects:
|
||||
## Debugging
|
||||
|
||||
### Setup for Debugging
|
||||
1. In Visual Studio 2022 or 2026, set FancyZonesEditor as the startup project
|
||||
1. In Visual Studio, set FancyZonesEditor as the startup project
|
||||
2. Set breakpoints in the code where needed
|
||||
3. Click Run to start debugging
|
||||
|
||||
|
||||
@@ -2,91 +2,31 @@
|
||||
|
||||
Welcome to the PowerToys developer documentation. This documentation provides information for developers who want to contribute to PowerToys or understand how it works.
|
||||
|
||||
## Core Architecture
|
||||
## Getting Started
|
||||
|
||||
- [Architecture Overview](core/architecture.md) - Overview of the PowerToys architecture and module interface
|
||||
- [Runner and System tray](core/runner.md) - Details about the PowerToys Runner process
|
||||
- [Settings](core/settings/readme.md) - Documentation on the settings system
|
||||
- [Installer](core/installer.md) - Information about the PowerToys installer
|
||||
- [Modules](modules/readme.md) - Documentation for individual PowerToys modules
|
||||
### Prerequisites
|
||||
|
||||
## Common Components
|
||||
1. Windows 10 April 2018 Update (version 1803) or newer
|
||||
1. [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) (recommended) or Visual Studio 2022 17.4+ with the following workloads/components:
|
||||
- Desktop Development with C++
|
||||
- WinUI application development
|
||||
- .NET desktop development
|
||||
- Windows 10 SDK (10.0.22621.0)
|
||||
- Windows 11 SDK (10.0.26100.3916)
|
||||
1. .NET 8 SDK
|
||||
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)
|
||||
|
||||
- [Context Menu Handlers](common/context-menus.md) - How PowerToys implements and registers Explorer context menu handlers
|
||||
- [Monaco Editor](common/monaco-editor.md) - How PowerToys uses the Monaco code editor component across modules
|
||||
- [Logging and Telemetry](development/logging.md) - How to use logging and telemetry
|
||||
- [Localization](development/localization.md) - How to support multiple languages
|
||||
> **Tip:** You can install Visual Studio with all required workloads automatically using the [WinGet configuration files](https://github.com/microsoft/PowerToys/tree/main/.config) in the repository:
|
||||
> ```powershell
|
||||
> winget configure .config\configuration.winget
|
||||
> ```
|
||||
> Pick the file that matches your VS edition (e.g., `configuration.vsProfessional.winget` or `configuration.vsEnterprise.winget`).
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- [Coding Guidelines](development/guidelines.md) - Development guidelines and best practices
|
||||
- [Coding Style](development/style.md) - Code formatting and style conventions
|
||||
- [UI Testing](development/ui-tests.md) - How to write UI tests for PowerToys
|
||||
- [Debugging](development/debugging.md) - Techniques for debugging PowerToys
|
||||
|
||||
## Tools
|
||||
|
||||
- [Tools Overview](tools/readme.md) - Overview of tools in PowerToys
|
||||
- [Build Tools](tools/build-tools.md) - Tools that help building PowerToys
|
||||
- [Bug Report Tool](tools/bug-report-tool.md) - Tool for collecting logs and system information
|
||||
- [Debugging Tools](tools/debugging-tools.md) - Specialized tools for debugging
|
||||
- [Fuzzing Testing](tools/fuzzingtesting.md) - How to implement and run fuzz testing for PowerToys modules
|
||||
|
||||
## Processes
|
||||
|
||||
- [Release Process](processes/release-process.md) - How PowerToys releases are prepared and published
|
||||
- [Update Process](processes/update-process.md) - How PowerToys updates work
|
||||
- [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details
|
||||
|
||||
## Other Resources
|
||||
|
||||
- [aka.ms links](akaLinks.md) - List of short links
|
||||
- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests
|
||||
|
||||
## Fork, Clone, Branch and Create your PR
|
||||
|
||||
Once you've discussed your proposed feature/fix/etc. with a team member, and an approach or a spec has been written and approved, it's time to start development:
|
||||
### Fork, Clone, and Set Up
|
||||
|
||||
1. Fork the repo on GitHub if you haven't already
|
||||
1. Clone your fork locally
|
||||
1. Create a feature branch
|
||||
1. Work on your changes
|
||||
1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/)
|
||||
1. When ready, mark your PR as "ready for review".
|
||||
|
||||
## Rules
|
||||
|
||||
- **Follow the pattern of what you already see in the code.**
|
||||
- [Coding style](development/style.md).
|
||||
- Try to package new functionality/components into libraries that have nicely defined interfaces.
|
||||
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
|
||||
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
|
||||
|
||||
## GitHub Workflow
|
||||
|
||||
- Before starting to work on a fix/feature, make sure there is an open issue to track the work.
|
||||
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
|
||||
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
|
||||
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
|
||||
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
|
||||
- When opening a PR, follow the PR template.
|
||||
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
|
||||
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.
|
||||
- Use the `Squash and merge` option to merge a PR. If you don't want to squash it because there are logically different commits, use `Rebase and merge`.
|
||||
- Close issues automatically when referenced in a PR. You can use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) in the body of the PR to have GitHub automatically link your PR to the issue.
|
||||
|
||||
## Compiling PowerToys
|
||||
|
||||
### Prerequisites for Compiling PowerToys
|
||||
|
||||
1. Windows 10 April 2018 Update (version 1803) or newer
|
||||
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer, or Visual Studio 2026
|
||||
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)
|
||||
|
||||
### Automated Setup (Recommended)
|
||||
|
||||
Run the setup script to automatically configure your development environment:
|
||||
1. Run the automated setup script (**recommended**):
|
||||
|
||||
```powershell
|
||||
.\tools\build\setup-dev-environment.ps1
|
||||
@@ -98,15 +38,10 @@ This script will:
|
||||
- Guide you through installing required Visual Studio components from `.vsconfig`
|
||||
- Initialize git submodules
|
||||
|
||||
Run with `-Help` to see all available options:
|
||||
Run with `-Help` to see all available options.
|
||||
|
||||
```powershell
|
||||
.\tools\build\setup-dev-environment.ps1 -Help
|
||||
```
|
||||
|
||||
### Manual Setup
|
||||
|
||||
If you prefer to set up manually, follow these steps:
|
||||
<details>
|
||||
<summary><strong>Manual setup (if you prefer not to use the script)</strong></summary>
|
||||
|
||||
#### Install Visual Studio dependencies
|
||||
|
||||
@@ -115,15 +50,17 @@ If you prefer to set up manually, follow these steps:
|
||||
|
||||
Alternatively, import the `.vsconfig` file from the repository root using Visual Studio Installer to install all required workloads.
|
||||
|
||||
#### Get Submodules to compile
|
||||
#### Initialize submodules
|
||||
|
||||
We have submodules that need to be initialized before you can compile most parts of PowerToys. This should be a one-time step.
|
||||
This is a one-time step required before you can compile most parts of PowerToys.
|
||||
|
||||
1. Open a terminal
|
||||
1. Navigate to the folder you cloned PowerToys to.
|
||||
1. Run `git submodule update --init --recursive`
|
||||
|
||||
### Compiling Source Code
|
||||
</details>
|
||||
|
||||
### Building
|
||||
|
||||
#### Using Visual Studio
|
||||
|
||||
@@ -151,7 +88,77 @@ You can also build from the command line using the provided scripts in `tools\bu
|
||||
.\tools\build\build-installer.ps1
|
||||
```
|
||||
|
||||
## Compile the installer
|
||||
### Debugging
|
||||
|
||||
See [Debugging](development/debugging.md) for detailed debugging techniques, including Visual Studio setup, attaching to child processes, and troubleshooting build errors.
|
||||
|
||||
### Creating a New PowerToy
|
||||
|
||||
See [Creating a New PowerToy](development/new-powertoy.md) for an end-to-end guide covering module architecture, settings integration, installer packaging, and testing.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- [Coding Guidelines](development/guidelines.md) - Development guidelines and best practices
|
||||
- [Coding Style](development/style.md) - Code formatting and style conventions
|
||||
- [Logging and Telemetry](development/logging.md) - How to use logging and telemetry
|
||||
- [Localization](development/localization.md) - How to support multiple languages
|
||||
- [UI Testing](development/ui-tests.md) - How to write UI tests for PowerToys
|
||||
- [Developing with VS Code](development/dev-with-vscode.md) - Build, debug, and contribute using VS Code
|
||||
|
||||
## Rules
|
||||
|
||||
- **Follow the pattern of what you already see in the code.**
|
||||
- [Coding style](development/style.md).
|
||||
- Try to package new functionality/components into libraries that have nicely defined interfaces.
|
||||
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
|
||||
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
|
||||
|
||||
## GitHub Workflow
|
||||
|
||||
- Before starting to work on a fix/feature, make sure there is an open issue to track the work.
|
||||
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
|
||||
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
|
||||
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
|
||||
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
|
||||
- When opening a PR, follow the PR template.
|
||||
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
|
||||
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.
|
||||
- Use the `Squash and merge` option to merge a PR. If you don't want to squash it because there are logically different commits, use `Rebase and merge`.
|
||||
- Close issues automatically when referenced in a PR. You can use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) in the body of the PR to have GitHub automatically link your PR to the issue.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
- [Architecture Overview](core/architecture.md) - Overview of the PowerToys architecture and module interface
|
||||
- [Runner and System tray](core/runner.md) - Details about the PowerToys Runner process
|
||||
- [Settings](core/settings/readme.md) - Documentation on the settings system
|
||||
- [Installer](core/installer.md) - Information about the PowerToys installer
|
||||
- [Modules](modules/readme.md) - Documentation for individual PowerToys modules
|
||||
|
||||
## Common Components
|
||||
|
||||
- [Context Menu Handlers](common/context-menus.md) - How PowerToys implements and registers Explorer context menu handlers
|
||||
- [Monaco Editor](common/monaco-editor.md) - How PowerToys uses the Monaco code editor component across modules
|
||||
|
||||
## Tools
|
||||
|
||||
- [Tools Overview](tools/readme.md) - Overview of tools in PowerToys
|
||||
- [Build Tools](tools/build-tools.md) - Tools that help building PowerToys
|
||||
- [Bug Report Tool](tools/bug-report-tool.md) - Tool for collecting logs and system information
|
||||
- [Debugging Tools](tools/debugging-tools.md) - Specialized tools for debugging
|
||||
- [Fuzzing Testing](tools/fuzzingtesting.md) - How to implement and run fuzz testing for PowerToys modules
|
||||
|
||||
## Processes
|
||||
|
||||
- [Release Process](processes/release-process.md) - How PowerToys releases are prepared and published
|
||||
- [Update Process](processes/update-process.md) - How PowerToys updates work
|
||||
- [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details
|
||||
|
||||
## Other Resources
|
||||
|
||||
- [aka.ms links](akaLinks.md) - List of short links
|
||||
- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests
|
||||
|
||||
## Building the Installer
|
||||
|
||||
Our installer is two parts, an EXE and an MSI. The EXE (Bootstrapper) contains the MSI and handles more complex installation logic.
|
||||
- The EXE installs all prerequisites and installs PowerToys via the MSI. It has additional features such as the installation flags (see below).
|
||||
@@ -165,8 +172,3 @@ The installer can only be compiled in `Release` mode; steps 1 and 2 must be perf
|
||||
1. Compile `PowerToysSetup.slnx` Path from root: `installer\PowerToysSetup.slnx` (details listed below)
|
||||
|
||||
See [Installer](core/installer.md) for more details on building and debugging the installer.
|
||||
|
||||
## How to create new PowerToys
|
||||
|
||||
See the instructions on [how to install the PowerToys Module project template](/tools/project_template). <br />
|
||||
Specifications for the [PowerToys settings API](core/settings/readme.md).
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -34,8 +34,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
}
|
||||
|
||||
// Check if model is in catalog
|
||||
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
if (!isInCatalog)
|
||||
if (!EnsureModelInCatalog(modelId))
|
||||
{
|
||||
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
|
||||
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||
@@ -43,15 +42,28 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
}
|
||||
|
||||
// Ensure the model is loaded before returning chat client
|
||||
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
var isLoaded = EnsureModelLoadedWithRefresh(modelId);
|
||||
if (!isLoaded)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
|
||||
}
|
||||
|
||||
var client = _foundryClient;
|
||||
if (client == null)
|
||||
{
|
||||
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
|
||||
var baseUri = _foundryClient.GetServiceUri();
|
||||
var baseUri = client.GetServiceUri();
|
||||
if (baseUri == null && TryRefreshClient("Service URI was not available"))
|
||||
{
|
||||
baseUri = _foundryClient?.GetServiceUri();
|
||||
}
|
||||
|
||||
if (baseUri == null)
|
||||
{
|
||||
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
|
||||
@@ -124,6 +136,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
|
||||
{
|
||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||
_serviceUrl = await _foundryClient.GetServiceUrl().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,4 +166,75 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
Logger.LogInfo($"[FoundryLocal] Available: {available}");
|
||||
return available;
|
||||
}
|
||||
|
||||
private bool EnsureModelInCatalog(string modelId)
|
||||
{
|
||||
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
if (isInCatalog)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Logger.LogWarning($"[FoundryLocal] Model not found in catalog. Refreshing client for model: {modelId}");
|
||||
if (!TryRefreshClient("Model not in catalog"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
}
|
||||
|
||||
private bool EnsureModelLoadedWithRefresh(string modelId)
|
||||
{
|
||||
var isLoaded = false;
|
||||
|
||||
try
|
||||
{
|
||||
isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[FoundryLocal] EnsureModelLoaded failed: {ex.Message}");
|
||||
}
|
||||
|
||||
if (isLoaded)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryRefreshClient("EnsureModelLoaded failed"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] EnsureModelLoaded failed after refresh: {ex.Message}", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRefreshClient(string reason)
|
||||
{
|
||||
Logger.LogInfo($"[FoundryLocal] Refreshing Foundry Local client: {reason}");
|
||||
|
||||
try
|
||||
{
|
||||
_foundryClient = null;
|
||||
_catalogModels = null;
|
||||
_serviceUrl = null;
|
||||
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
return _foundryClient != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to refresh Foundry Local client: {ex.Message}", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the packaging flavor of the application.
|
||||
/// </summary>
|
||||
public enum AppPackagingFlavor
|
||||
{
|
||||
/// <summary>
|
||||
/// Application is packaged as a Windows MSIX package.
|
||||
/// </summary>
|
||||
Packaged,
|
||||
|
||||
/// <summary>
|
||||
/// Application is running unpackaged (native executable).
|
||||
/// </summary>
|
||||
Unpackaged,
|
||||
|
||||
/// <summary>
|
||||
/// Application is running as unpackaged portable (self-contained distribution).
|
||||
/// </summary>
|
||||
UnpackagedPortable,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an item that can provide precomputed fuzzy matching targets for its title and subtitle.
|
||||
/// </summary>
|
||||
public interface IPrecomputedListItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the fuzzy matching target for the item's title.
|
||||
/// </summary>
|
||||
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
|
||||
/// <returns>The fuzzy target for the title.</returns>
|
||||
FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fuzzy matching target for the item's subtitle.
|
||||
/// </summary>
|
||||
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
|
||||
/// <returns>The fuzzy target for the subtitle.</returns>
|
||||
FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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.Buffers;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
public static partial class InternalListHelpers
|
||||
{
|
||||
public static RoScored<T>[] FilterListWithScores<T>(
|
||||
IEnumerable<T>? items,
|
||||
in FuzzyQuery query,
|
||||
in ScoringFunction<T> scoreFunction)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try to get initial capacity hint
|
||||
var initialCapacity = items switch
|
||||
{
|
||||
ICollection<T> col => col.Count,
|
||||
IReadOnlyCollection<T> rc => rc.Count,
|
||||
_ => 64,
|
||||
};
|
||||
|
||||
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
|
||||
var count = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
var score = scoreFunction(in query, item);
|
||||
if (score <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count == buffer.Length)
|
||||
{
|
||||
GrowBuffer(ref buffer, count);
|
||||
}
|
||||
|
||||
buffer[count++] = new RoScored<T>(item, score);
|
||||
}
|
||||
|
||||
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
|
||||
var result = GC.AllocateUninitializedArray<RoScored<T>>(count);
|
||||
buffer.AsSpan(0, count).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<RoScored<T>>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrowBuffer<T>(ref RoScored<T>[] buffer, int count)
|
||||
{
|
||||
var newBuffer = ArrayPool<RoScored<T>>.Shared.Rent(buffer.Length * 2);
|
||||
buffer.AsSpan(0, count).CopyTo(newBuffer);
|
||||
ArrayPool<RoScored<T>>.Shared.Return(buffer);
|
||||
buffer = newBuffer;
|
||||
}
|
||||
|
||||
public static T[] FilterList<T>(IEnumerable<T> items, in FuzzyQuery query, ScoringFunction<T> scoreFunction)
|
||||
{
|
||||
// Try to get initial capacity hint
|
||||
var initialCapacity = items switch
|
||||
{
|
||||
ICollection<T> col => col.Count,
|
||||
IReadOnlyCollection<T> rc => rc.Count,
|
||||
_ => 64,
|
||||
};
|
||||
|
||||
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
|
||||
var count = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
var score = scoreFunction(in query, item);
|
||||
if (score <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count == buffer.Length)
|
||||
{
|
||||
GrowBuffer(ref buffer, count);
|
||||
}
|
||||
|
||||
buffer[count++] = new RoScored<T>(item, score);
|
||||
}
|
||||
|
||||
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
|
||||
|
||||
var result = GC.AllocateUninitializedArray<T>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
result[i] = buffer[i].Item;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<RoScored<T>>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct RoScoredDescendingComparer<T> : IComparer<RoScored<T>>
|
||||
{
|
||||
public int Compare(RoScored<T> x, RoScored<T> y) => y.Score.CompareTo(x.Score);
|
||||
}
|
||||
}
|
||||
|
||||
public delegate int ScoringFunction<in T>(in FuzzyQuery query, T item);
|
||||
|
||||
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
|
||||
public readonly struct RoScored<T>
|
||||
{
|
||||
public readonly int Score;
|
||||
public readonly T Item;
|
||||
|
||||
public RoScored(T item, int score)
|
||||
{
|
||||
Score = score;
|
||||
Item = item;
|
||||
}
|
||||
|
||||
private string GetDebuggerDisplay()
|
||||
{
|
||||
return "Score = " + Score + ", Item = " + Item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for retrieving application version information safely.
|
||||
/// </summary>
|
||||
internal static class VersionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
|
||||
/// Falls back to assembly version if packaged version is unavailable, and returns a default value if both fail.
|
||||
/// </summary>
|
||||
/// <returns>The application version string, or a fallback value if retrieval fails.</returns>
|
||||
public static string GetAppVersionSafe()
|
||||
{
|
||||
if (TryGetPackagedVersion(out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
if (TryGetAssemblyVersion(out version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the application version from the package manifest.
|
||||
/// </summary>
|
||||
/// <param name="version">The version string if successful, or an empty string if unsuccessful.</param>
|
||||
/// <returns>True if the version was retrieved successfully; otherwise, false.</returns>
|
||||
private static bool TryGetPackagedVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
// Package.Current throws InvalidOperationException if the app is not packaged
|
||||
var v = Package.Current.Id.Version;
|
||||
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to get version from the package", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the application version from the executable file.
|
||||
/// </summary>
|
||||
/// <param name="version">The version string if successful, or an empty string if unsuccessful.</param>
|
||||
/// <returns>True if the version was retrieved successfully; otherwise, false.</returns>
|
||||
private static bool TryGetAssemblyVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(processPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var info = FileVersionInfo.GetVersionInfo(processPath);
|
||||
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to get version from the executable", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of IApplicationInfoService providing application-wide information.
|
||||
/// </summary>
|
||||
public sealed class ApplicationInfoService : IApplicationInfoService
|
||||
{
|
||||
private readonly Lazy<string> _configDirectory = new(() => Utilities.BaseSettingsPath("Microsoft.CmdPal"));
|
||||
private readonly Lazy<bool> _isElevated;
|
||||
private readonly Lazy<string> _logDirectory;
|
||||
private readonly Lazy<AppPackagingFlavor> _packagingFlavor;
|
||||
private Func<string>? _getLogDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationInfoService"/> class.
|
||||
/// The log directory delegate can be set later via <see cref="SetLogDirectory(Func{string})"/>.
|
||||
/// </summary>
|
||||
public ApplicationInfoService()
|
||||
{
|
||||
_packagingFlavor = new Lazy<AppPackagingFlavor>(DeterminePackagingFlavor);
|
||||
_isElevated = new Lazy<bool>(DetermineElevationStatus);
|
||||
_logDirectory = new Lazy<string>(() => _getLogDirectory?.Invoke() ?? "Not available");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationInfoService"/> class with an optional log directory provider.
|
||||
/// </summary>
|
||||
/// <param name="getLogDirectory">Optional delegate to retrieve the log directory path. If not provided, the log directory will be unavailable.</param>
|
||||
public ApplicationInfoService(Func<string>? getLogDirectory)
|
||||
: this()
|
||||
{
|
||||
_getLogDirectory = getLogDirectory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the log directory delegate to be used for retrieving the log directory path.
|
||||
/// This allows deferred initialization of the logger path.
|
||||
/// </summary>
|
||||
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
|
||||
public void SetLogDirectory(Func<string> getLogDirectory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getLogDirectory);
|
||||
_getLogDirectory = getLogDirectory;
|
||||
}
|
||||
|
||||
public string AppVersion => VersionHelper.GetAppVersionSafe();
|
||||
|
||||
public AppPackagingFlavor PackagingFlavor => _packagingFlavor.Value;
|
||||
|
||||
public string LogDirectory => _logDirectory.Value;
|
||||
|
||||
public string ConfigDirectory => _configDirectory.Value;
|
||||
|
||||
public bool IsElevated => _isElevated.Value;
|
||||
|
||||
public string GetApplicationInfoSummary()
|
||||
{
|
||||
return $"""
|
||||
Application:
|
||||
App version: {AppVersion}
|
||||
Packaging flavor: {PackagingFlavor}
|
||||
Is elevated: {(IsElevated ? "yes" : "no")}
|
||||
|
||||
Environment:
|
||||
OS version: {RuntimeInformation.OSDescription}
|
||||
OS architecture: {RuntimeInformation.OSArchitecture}
|
||||
Runtime identifier: {RuntimeInformation.RuntimeIdentifier}
|
||||
Framework: {RuntimeInformation.FrameworkDescription}
|
||||
Process architecture: {RuntimeInformation.ProcessArchitecture}
|
||||
Culture: {CultureInfo.CurrentCulture.Name}
|
||||
UI culture: {CultureInfo.CurrentUICulture.Name}
|
||||
|
||||
Paths:
|
||||
Log directory: {LogDirectory}
|
||||
Config directory: {ConfigDirectory}
|
||||
""";
|
||||
}
|
||||
|
||||
private static AppPackagingFlavor DeterminePackagingFlavor()
|
||||
{
|
||||
// Try to determine if running as packaged
|
||||
try
|
||||
{
|
||||
// If this doesn't throw, we're packaged
|
||||
_ = Package.Current.Id.Version;
|
||||
return AppPackagingFlavor.Packaged;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Not packaged, check if portable
|
||||
// For now, we don't support portable yet, so return Unpackaged
|
||||
// In the future, check for a marker file or environment variable
|
||||
return AppPackagingFlavor.Unpackaged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to determine packaging flavor", ex);
|
||||
return AppPackagingFlavor.Unpackaged;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DetermineElevationStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
return isElevated;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to application-wide information such as version, packaging flavor, and directory paths.
|
||||
/// </summary>
|
||||
public interface IApplicationInfoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
|
||||
/// </summary>
|
||||
string AppVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the packaging flavor of the application.
|
||||
/// </summary>
|
||||
AppPackagingFlavor PackagingFlavor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory path where application logs are stored.
|
||||
/// </summary>
|
||||
string LogDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory path where application configuration files are stored.
|
||||
/// </summary>
|
||||
string ConfigDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the application is running with administrator privileges.
|
||||
/// </summary>
|
||||
bool IsElevated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted summary of application information suitable for logging.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string containing application information.</returns>
|
||||
string GetApplicationInfoSummary();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the log directory delegate to be used for retrieving the log directory path.
|
||||
/// This allows deferred initialization of the logger path.
|
||||
/// </summary>
|
||||
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
|
||||
void SetLogDirectory(Func<string> getLogDirectory);
|
||||
}
|
||||
@@ -2,20 +2,27 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||
|
||||
public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
{
|
||||
private readonly ErrorReportSanitizer _sanitizer = new();
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
|
||||
private static string Preamble => Properties.Resources.ErrorReport_Global_Preamble;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ErrorReportBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="appInfoService">Optional application info service. If not provided, a default instance is created.</param>
|
||||
public ErrorReportBuilder(IApplicationInfoService? appInfoService = null)
|
||||
{
|
||||
_appInfoService = appInfoService ?? new ApplicationInfoService(null);
|
||||
}
|
||||
|
||||
public string BuildReport(Exception exception, string context, bool redactPii = true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
@@ -24,6 +31,9 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
var sanitizedMessage = redactPii ? _sanitizer.Sanitize(exceptionMessage) : exceptionMessage;
|
||||
var sanitizedFormattedException = redactPii ? _sanitizer.Sanitize(exception.ToString()) : exception.ToString();
|
||||
|
||||
var applicationInfoSummary = GetAppInfoSafe();
|
||||
var applicationInfoSummarySanitized = redactPii ? _sanitizer.Sanitize(applicationInfoSummary) : applicationInfoSummary;
|
||||
|
||||
// Note:
|
||||
// - do not localize technical part of the report, we need to ensure it can be read by developers
|
||||
// - keep timestamp format should be consistent with the log (makes it easier to search)
|
||||
@@ -38,18 +48,7 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
|
||||
Context: {context ?? "N/A"}
|
||||
|
||||
Application:
|
||||
App version: {GetAppVersionSafe()}
|
||||
Is elevated: {GetElevationStatus()}
|
||||
|
||||
Environment:
|
||||
OS version: {RuntimeInformation.OSDescription}
|
||||
OS architecture: {RuntimeInformation.OSArchitecture}
|
||||
Runtime identifier: {RuntimeInformation.RuntimeIdentifier}
|
||||
Framework: {RuntimeInformation.FrameworkDescription}
|
||||
Process architecture: {RuntimeInformation.ProcessArchitecture}
|
||||
Culture: {CultureInfo.CurrentCulture.Name}
|
||||
UI culture: {CultureInfo.CurrentUICulture.Name}
|
||||
{applicationInfoSummarySanitized}
|
||||
|
||||
Stack Trace:
|
||||
{exception.StackTrace}
|
||||
@@ -66,31 +65,17 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
""";
|
||||
}
|
||||
|
||||
private static string GetElevationStatus()
|
||||
private string? GetAppInfoSafe()
|
||||
{
|
||||
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
|
||||
try
|
||||
{
|
||||
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
return isElevated ? "yes" : "no";
|
||||
return _appInfoService.GetApplicationInfoSummary();
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return "Failed to determine elevation status";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAppVersionSafe()
|
||||
{
|
||||
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
|
||||
try
|
||||
{
|
||||
var version = Package.Current.Id.Version;
|
||||
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return "Failed to retrieve app version";
|
||||
// Getting application info should never throw, but if it does, we don't want it to prevent the report from being generated
|
||||
var message = CoalesceExceptionMessage(ex);
|
||||
return $"Failed to get application info summary: {message}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class BloomFilter : IBloomFilter
|
||||
{
|
||||
public ulong Compute(string input)
|
||||
{
|
||||
ulong bloom = 0;
|
||||
|
||||
foreach (var ch in input)
|
||||
{
|
||||
if (SymbolClassifier.Classify(ch) == SymbolKind.WordSeparator)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var h = (uint)ch * 0x45d9f3b;
|
||||
bloom |= 1UL << (int)(h & 31);
|
||||
bloom |= 1UL << (int)(((h >> 16) & 31) + 32);
|
||||
|
||||
if (bloom == ulong.MaxValue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bloom;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MightContain(ulong candidateBloom, ulong queryBloom)
|
||||
{
|
||||
return (candidateBloom & queryBloom) == queryBloom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class FuzzyMatcherProvider : IFuzzyMatcherProvider
|
||||
{
|
||||
private readonly IBloomFilter _bloomCalculator = new BloomFilter();
|
||||
private readonly IStringFolder _normalizer = new StringFolder();
|
||||
|
||||
private IPrecomputedFuzzyMatcher _current;
|
||||
|
||||
public FuzzyMatcherProvider(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
|
||||
{
|
||||
_current = CreateMatcher(core, pinyin);
|
||||
}
|
||||
|
||||
public IPrecomputedFuzzyMatcher Current => Volatile.Read(ref _current);
|
||||
|
||||
public void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
|
||||
{
|
||||
Volatile.Write(ref _current, CreateMatcher(core, pinyin));
|
||||
}
|
||||
|
||||
private IPrecomputedFuzzyMatcher CreateMatcher(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin)
|
||||
{
|
||||
return pinyin is null || !IsPinyinEnabled(pinyin)
|
||||
? new PrecomputedFuzzyMatcher(core, _normalizer, _bloomCalculator)
|
||||
: new PrecomputedFuzzyMatcherWithPinyin(core, pinyin, _normalizer, _bloomCalculator);
|
||||
}
|
||||
|
||||
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o)
|
||||
{
|
||||
return o.Mode switch
|
||||
{
|
||||
PinyinMode.Off => false,
|
||||
PinyinMode.On => true,
|
||||
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsSimplifiedChineseUi()
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public readonly struct FuzzyQuery
|
||||
{
|
||||
public readonly string Original;
|
||||
|
||||
public readonly string Folded;
|
||||
|
||||
public readonly ulong Bloom;
|
||||
|
||||
public readonly int EffectiveLength;
|
||||
|
||||
public readonly bool IsAllLowercaseAsciiOrNonLetter;
|
||||
|
||||
public readonly string? SecondaryOriginal;
|
||||
|
||||
public readonly string? SecondaryFolded;
|
||||
|
||||
public readonly ulong SecondaryBloom;
|
||||
|
||||
public readonly int SecondaryEffectiveLength;
|
||||
|
||||
public readonly bool SecondaryIsAllLowercaseAsciiOrNonLetter;
|
||||
|
||||
public int Length => Folded.Length;
|
||||
|
||||
public bool HasSecondary => SecondaryFolded is not null;
|
||||
|
||||
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
|
||||
|
||||
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
|
||||
|
||||
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
|
||||
|
||||
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
|
||||
|
||||
public FuzzyQuery(
|
||||
string original,
|
||||
string folded,
|
||||
ulong bloom,
|
||||
int effectiveLength,
|
||||
bool isAllLowercaseAsciiOrNonLetter,
|
||||
string? secondaryOriginal = null,
|
||||
string? secondaryFolded = null,
|
||||
ulong secondaryBloom = 0,
|
||||
int secondaryEffectiveLength = 0,
|
||||
bool secondaryIsAllLowercaseAsciiOrNonLetter = true)
|
||||
{
|
||||
Original = original;
|
||||
Folded = folded;
|
||||
Bloom = bloom;
|
||||
EffectiveLength = effectiveLength;
|
||||
IsAllLowercaseAsciiOrNonLetter = isAllLowercaseAsciiOrNonLetter;
|
||||
|
||||
SecondaryOriginal = secondaryOriginal;
|
||||
SecondaryFolded = secondaryFolded;
|
||||
SecondaryBloom = secondaryBloom;
|
||||
SecondaryEffectiveLength = secondaryEffectiveLength;
|
||||
SecondaryIsAllLowercaseAsciiOrNonLetter = secondaryIsAllLowercaseAsciiOrNonLetter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public readonly struct FuzzyTarget
|
||||
{
|
||||
public readonly string Original;
|
||||
public readonly string Folded;
|
||||
public readonly ulong Bloom;
|
||||
|
||||
public readonly string? SecondaryOriginal;
|
||||
public readonly string? SecondaryFolded;
|
||||
public readonly ulong SecondaryBloom;
|
||||
|
||||
public int Length => Folded.Length;
|
||||
|
||||
public bool HasSecondary => SecondaryFolded is not null;
|
||||
|
||||
public int SecondaryLength => SecondaryFolded?.Length ?? 0;
|
||||
|
||||
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
|
||||
|
||||
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
|
||||
|
||||
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
|
||||
|
||||
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
|
||||
|
||||
public FuzzyTarget(
|
||||
string original,
|
||||
string folded,
|
||||
ulong bloom,
|
||||
string? secondaryOriginal = null,
|
||||
string? secondaryFolded = null,
|
||||
ulong secondaryBloom = 0)
|
||||
{
|
||||
Original = original;
|
||||
Folded = folded;
|
||||
Bloom = bloom;
|
||||
SecondaryOriginal = secondaryOriginal;
|
||||
SecondaryFolded = secondaryFolded;
|
||||
SecondaryBloom = secondaryBloom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public struct FuzzyTargetCache
|
||||
{
|
||||
private string? _lastRaw;
|
||||
private uint _schemaId;
|
||||
private FuzzyTarget _target;
|
||||
|
||||
public FuzzyTarget GetOrUpdate(IPrecomputedFuzzyMatcher matcher, string? raw)
|
||||
{
|
||||
raw ??= string.Empty;
|
||||
|
||||
if (_schemaId == matcher.SchemaId && string.Equals(_lastRaw, raw, StringComparison.Ordinal))
|
||||
{
|
||||
return _target;
|
||||
}
|
||||
|
||||
_target = matcher.PrecomputeTarget(raw);
|
||||
_schemaId = matcher.SchemaId;
|
||||
_lastRaw = raw;
|
||||
return _target;
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
_lastRaw = null;
|
||||
_target = default;
|
||||
_schemaId = 0;
|
||||
}
|
||||
}
|
||||
@@ -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.Core.Common.Text;
|
||||
|
||||
public interface IBloomFilter
|
||||
{
|
||||
ulong Compute(string input);
|
||||
|
||||
bool MightContain(ulong candidateBloom, ulong queryBloom);
|
||||
}
|
||||
@@ -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.Core.Common.Text;
|
||||
|
||||
public interface IFuzzyMatcherProvider
|
||||
{
|
||||
IPrecomputedFuzzyMatcher Current { get; }
|
||||
|
||||
void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public interface IPrecomputedFuzzyMatcher
|
||||
{
|
||||
uint SchemaId { get; }
|
||||
|
||||
FuzzyQuery PrecomputeQuery(string? input);
|
||||
|
||||
FuzzyTarget PrecomputeTarget(string? input);
|
||||
|
||||
int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public interface IStringFolder
|
||||
{
|
||||
string Fold(string input, bool removeDiacritics);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class PinyinFuzzyMatcherOptions
|
||||
{
|
||||
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
|
||||
|
||||
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
|
||||
public bool RemoveApostrophesForQuery { get; init; } = true;
|
||||
}
|
||||
@@ -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.Core.Common.Text;
|
||||
|
||||
public enum PinyinMode
|
||||
{
|
||||
Off = 0,
|
||||
AutoSimplifiedChineseUi = 1,
|
||||
On = 2,
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
// 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.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class PrecomputedFuzzyMatcher : IPrecomputedFuzzyMatcher
|
||||
{
|
||||
private const int NoMatchScore = 0;
|
||||
private const int StackallocThresholdChars = 512;
|
||||
private const int FolderSchemaVersion = 1;
|
||||
private const int BloomSchemaVersion = 1;
|
||||
|
||||
private readonly PrecomputedFuzzyMatcherOptions _options;
|
||||
private readonly IStringFolder _stringFolder;
|
||||
private readonly IBloomFilter _bloom;
|
||||
|
||||
public PrecomputedFuzzyMatcher(
|
||||
PrecomputedFuzzyMatcherOptions? options = null,
|
||||
IStringFolder? normalization = null,
|
||||
IBloomFilter? bloomCalculator = null)
|
||||
{
|
||||
_options = options ?? PrecomputedFuzzyMatcherOptions.Default;
|
||||
_bloom = bloomCalculator ?? new BloomFilter();
|
||||
_stringFolder = normalization ?? new StringFolder();
|
||||
|
||||
SchemaId = ComputeSchemaId(_options);
|
||||
}
|
||||
|
||||
public uint SchemaId { get; }
|
||||
|
||||
public FuzzyQuery PrecomputeQuery(string? input) => PrecomputeQuery(input, null);
|
||||
|
||||
public FuzzyTarget PrecomputeTarget(string? input) => PrecomputeTarget(input, null);
|
||||
|
||||
public int Score(in FuzzyQuery query, in FuzzyTarget target)
|
||||
{
|
||||
var qFold = query.FoldedSpan;
|
||||
var tLen = target.Length;
|
||||
|
||||
if (query.EffectiveLength == 0 || tLen == 0)
|
||||
{
|
||||
return NoMatchScore;
|
||||
}
|
||||
|
||||
var skipWordSeparators = _options.SkipWordSeparators;
|
||||
var bestScore = 0;
|
||||
|
||||
// 1. Primary → Primary
|
||||
if (tLen >= query.EffectiveLength && _bloom.MightContain(target.Bloom, query.Bloom))
|
||||
{
|
||||
if (CanMatchSubsequence(qFold, target.FoldedSpan, skipWordSeparators))
|
||||
{
|
||||
bestScore = ScoreNonContiguous(
|
||||
qRaw: query.OriginalSpan,
|
||||
qFold: qFold,
|
||||
qEffectiveLen: query.EffectiveLength,
|
||||
tRaw: target.OriginalSpan,
|
||||
tFold: target.FoldedSpan,
|
||||
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Secondary → Secondary
|
||||
if (query.HasSecondary && target.HasSecondary)
|
||||
{
|
||||
var qSecFold = query.SecondaryFoldedSpan;
|
||||
|
||||
if (target.SecondaryLength >= query.SecondaryEffectiveLength &&
|
||||
_bloom.MightContain(target.SecondaryBloom, query.SecondaryBloom) &&
|
||||
CanMatchSubsequence(qSecFold, target.SecondaryFoldedSpan, skipWordSeparators))
|
||||
{
|
||||
var score = ScoreNonContiguous(
|
||||
qRaw: query.SecondaryOriginalSpan,
|
||||
qFold: qSecFold,
|
||||
qEffectiveLen: query.SecondaryEffectiveLength,
|
||||
tRaw: target.SecondaryOriginalSpan,
|
||||
tFold: target.SecondaryFoldedSpan,
|
||||
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Primary query → Secondary target
|
||||
if (target.HasSecondary &&
|
||||
target.SecondaryLength >= query.EffectiveLength &&
|
||||
_bloom.MightContain(target.SecondaryBloom, query.Bloom))
|
||||
{
|
||||
if (CanMatchSubsequence(qFold, target.SecondaryFoldedSpan, skipWordSeparators))
|
||||
{
|
||||
var score = ScoreNonContiguous(
|
||||
qRaw: query.OriginalSpan,
|
||||
qFold: qFold,
|
||||
qEffectiveLen: query.EffectiveLength,
|
||||
tRaw: target.SecondaryOriginalSpan,
|
||||
tFold: target.SecondaryFoldedSpan,
|
||||
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Secondary query → Primary target
|
||||
if (query.HasSecondary &&
|
||||
tLen >= query.SecondaryEffectiveLength &&
|
||||
_bloom.MightContain(target.Bloom, query.SecondaryBloom))
|
||||
{
|
||||
var qSecFold = query.SecondaryFoldedSpan;
|
||||
|
||||
if (CanMatchSubsequence(qSecFold, target.FoldedSpan, skipWordSeparators))
|
||||
{
|
||||
var score = ScoreNonContiguous(
|
||||
qRaw: query.SecondaryOriginalSpan,
|
||||
qFold: qSecFold,
|
||||
qEffectiveLen: query.SecondaryEffectiveLength,
|
||||
tRaw: target.OriginalSpan,
|
||||
tFold: target.FoldedSpan,
|
||||
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestScore;
|
||||
}
|
||||
|
||||
private FuzzyQuery PrecomputeQuery(string? input, string? secondaryInput)
|
||||
{
|
||||
input ??= string.Empty;
|
||||
|
||||
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
|
||||
var bloom = _bloom.Compute(folded);
|
||||
var effectiveLength = _options.SkipWordSeparators
|
||||
? folded.Length - CountWordSeparators(folded)
|
||||
: folded.Length;
|
||||
|
||||
var isAllLowercase = IsAllLowercaseAsciiOrNonLetter(input);
|
||||
|
||||
string? secondaryOriginal = null;
|
||||
string? secondaryFolded = null;
|
||||
ulong secondaryBloom = 0;
|
||||
var secondaryEffectiveLength = 0;
|
||||
var secondaryIsAllLowercase = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(secondaryInput))
|
||||
{
|
||||
secondaryOriginal = secondaryInput;
|
||||
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
|
||||
secondaryBloom = _bloom.Compute(secondaryFolded);
|
||||
secondaryEffectiveLength = _options.SkipWordSeparators
|
||||
? secondaryFolded.Length - CountWordSeparators(secondaryFolded)
|
||||
: secondaryFolded.Length;
|
||||
|
||||
secondaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(secondaryInput);
|
||||
}
|
||||
|
||||
return new FuzzyQuery(
|
||||
original: input,
|
||||
folded: folded,
|
||||
bloom: bloom,
|
||||
effectiveLength: effectiveLength,
|
||||
isAllLowercaseAsciiOrNonLetter: isAllLowercase,
|
||||
secondaryOriginal: secondaryOriginal,
|
||||
secondaryFolded: secondaryFolded,
|
||||
secondaryBloom: secondaryBloom,
|
||||
secondaryEffectiveLength: secondaryEffectiveLength,
|
||||
secondaryIsAllLowercaseAsciiOrNonLetter: secondaryIsAllLowercase);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static int CountWordSeparators(string s)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (SymbolClassifier.Classify(c) == SymbolKind.WordSeparator)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
internal FuzzyTarget PrecomputeTarget(string? input, string? secondaryInput)
|
||||
{
|
||||
input ??= string.Empty;
|
||||
|
||||
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
|
||||
var bloom = _bloom.Compute(folded);
|
||||
|
||||
string? secondaryFolded = null;
|
||||
ulong secondaryBloom = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(secondaryInput))
|
||||
{
|
||||
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
|
||||
secondaryBloom = _bloom.Compute(secondaryFolded);
|
||||
}
|
||||
|
||||
return new FuzzyTarget(
|
||||
input,
|
||||
folded,
|
||||
bloom,
|
||||
secondaryInput,
|
||||
secondaryFolded,
|
||||
secondaryBloom);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllLowercaseAsciiOrNonLetter(string s)
|
||||
{
|
||||
foreach (var c in s)
|
||||
{
|
||||
if ((uint)(c - 'A') <= ('Z' - 'A'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool CanMatchSubsequence(
|
||||
ReadOnlySpan<char> qFold,
|
||||
ReadOnlySpan<char> tFold,
|
||||
bool skipWordSeparators)
|
||||
{
|
||||
var qi = 0;
|
||||
var ti = 0;
|
||||
|
||||
while (qi < qFold.Length && ti < tFold.Length)
|
||||
{
|
||||
var qChar = qFold[qi];
|
||||
|
||||
if (skipWordSeparators && SymbolClassifier.Classify(qChar) == SymbolKind.WordSeparator)
|
||||
{
|
||||
qi++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (qChar == tFold[ti])
|
||||
{
|
||||
qi++;
|
||||
}
|
||||
|
||||
ti++;
|
||||
}
|
||||
|
||||
// Skip trailing word separators in query
|
||||
if (skipWordSeparators)
|
||||
{
|
||||
while (qi < qFold.Length && SymbolClassifier.Classify(qFold[qi]) == SymbolKind.WordSeparator)
|
||||
{
|
||||
qi++;
|
||||
}
|
||||
}
|
||||
|
||||
return qi == qFold.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
[SkipLocalsInit]
|
||||
private int ScoreNonContiguous(
|
||||
scoped in ReadOnlySpan<char> qRaw,
|
||||
scoped in ReadOnlySpan<char> qFold,
|
||||
int qEffectiveLen,
|
||||
scoped in ReadOnlySpan<char> tRaw,
|
||||
scoped in ReadOnlySpan<char> tFold,
|
||||
bool ignoreSameCaseBonusForThisQuery)
|
||||
{
|
||||
Debug.Assert(qRaw.Length == qFold.Length, "Original and folded spans are traversed in lockstep: requires qRaw.Length == qFold.Length");
|
||||
Debug.Assert(tRaw.Length == tFold.Length, "Original and folded spans are traversed in lockstep: requires tRaw.Length == tFold.Length");
|
||||
Debug.Assert(qEffectiveLen <= qFold.Length, "Effective length must be less than or equal to folded length");
|
||||
|
||||
var qLen = qFold.Length;
|
||||
var tLen = tFold.Length;
|
||||
|
||||
// Copy options to local variables to avoid repeated field accesses
|
||||
var charMatchBonus = _options.CharMatchBonus;
|
||||
var sameCaseBonus = ignoreSameCaseBonusForThisQuery ? 0 : _options.SameCaseBonus;
|
||||
var consecutiveMultiplier = _options.ConsecutiveMultiplier;
|
||||
var camelCaseBonus = _options.CamelCaseBonus;
|
||||
var startOfWordBonus = _options.StartOfWordBonus;
|
||||
var pathSeparatorBonus = _options.PathSeparatorBonus;
|
||||
var wordSeparatorBonus = _options.WordSeparatorBonus;
|
||||
var separatorAlignmentBonus = _options.SeparatorAlignmentBonus;
|
||||
var exactSeparatorBonus = _options.ExactSeparatorBonus;
|
||||
var skipWordSeparators = _options.SkipWordSeparators;
|
||||
|
||||
// DP buffer: two rows of length tLen
|
||||
var bufferSize = tLen * 2;
|
||||
int[]? rented = null;
|
||||
|
||||
try
|
||||
{
|
||||
scoped Span<int> buffer;
|
||||
if (bufferSize <= StackallocThresholdChars)
|
||||
{
|
||||
buffer = stackalloc int[bufferSize];
|
||||
}
|
||||
else
|
||||
{
|
||||
rented = ArrayPool<int>.Shared.Rent(bufferSize);
|
||||
buffer = rented.AsSpan(0, bufferSize);
|
||||
}
|
||||
|
||||
var scores = buffer[..tLen];
|
||||
var seqLens = buffer.Slice(tLen, tLen);
|
||||
|
||||
scores.Clear();
|
||||
seqLens.Clear();
|
||||
|
||||
ref var scores0 = ref MemoryMarshal.GetReference(scores);
|
||||
ref var seqLens0 = ref MemoryMarshal.GetReference(seqLens);
|
||||
ref var qRaw0 = ref MemoryMarshal.GetReference(qRaw);
|
||||
ref var qFold0 = ref MemoryMarshal.GetReference(qFold);
|
||||
ref var tRaw0 = ref MemoryMarshal.GetReference(tRaw);
|
||||
ref var tFold0 = ref MemoryMarshal.GetReference(tFold);
|
||||
|
||||
var qiEffective = 0;
|
||||
|
||||
for (var qi = 0; qi < qLen; qi++)
|
||||
{
|
||||
var qCharFold = Unsafe.Add(ref qFold0, qi);
|
||||
var qCharKind = SymbolClassifier.Classify(qCharFold);
|
||||
|
||||
if (skipWordSeparators && qCharKind == SymbolKind.WordSeparator)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hoisted values
|
||||
var qRawIsUpper = char.IsUpper(Unsafe.Add(ref qRaw0, qi));
|
||||
|
||||
// row computation
|
||||
var leftScore = 0;
|
||||
var diagScore = 0;
|
||||
var diagSeqLen = 0;
|
||||
|
||||
// limit ti to ensure enough remaining characters to match the rest of the query
|
||||
var tiMax = tLen - qEffectiveLen + qiEffective;
|
||||
|
||||
for (var ti = 0; ti <= tiMax; ti++)
|
||||
{
|
||||
var upScore = Unsafe.Add(ref scores0, ti);
|
||||
var upSeqLen = Unsafe.Add(ref seqLens0, ti);
|
||||
|
||||
var charScore = 0;
|
||||
if (diagScore != 0 || qiEffective == 0)
|
||||
{
|
||||
charScore = ComputeCharScore(
|
||||
qi,
|
||||
ti,
|
||||
qCharFold,
|
||||
qCharKind,
|
||||
diagSeqLen,
|
||||
qRawIsUpper,
|
||||
ref tRaw0,
|
||||
ref qFold0,
|
||||
ref tFold0);
|
||||
}
|
||||
|
||||
var candidateScore = diagScore + charScore;
|
||||
if (charScore != 0 && candidateScore >= leftScore)
|
||||
{
|
||||
Unsafe.Add(ref scores0, ti) = candidateScore;
|
||||
Unsafe.Add(ref seqLens0, ti) = diagSeqLen + 1;
|
||||
leftScore = candidateScore;
|
||||
}
|
||||
else
|
||||
{
|
||||
Unsafe.Add(ref scores0, ti) = leftScore;
|
||||
Unsafe.Add(ref seqLens0, ti) = 0;
|
||||
/* leftScore remains unchanged */
|
||||
}
|
||||
|
||||
diagScore = upScore;
|
||||
diagSeqLen = upSeqLen;
|
||||
}
|
||||
|
||||
// Early exit: no match possible
|
||||
if (leftScore == 0)
|
||||
{
|
||||
return NoMatchScore;
|
||||
}
|
||||
|
||||
// Advance effective query index
|
||||
// Only counts non-separator characters if skipWordSeparators is enabled
|
||||
qiEffective++;
|
||||
|
||||
if (qiEffective == qEffectiveLen)
|
||||
{
|
||||
return leftScore;
|
||||
}
|
||||
}
|
||||
|
||||
return scores[tLen - 1];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
int ComputeCharScore(
|
||||
int qi,
|
||||
int ti,
|
||||
char qCharFold,
|
||||
SymbolKind qCharKind,
|
||||
int seqLen,
|
||||
bool qCharRawCurrIsUpper,
|
||||
ref char tRaw0,
|
||||
ref char qFold0,
|
||||
ref char tFold0)
|
||||
{
|
||||
// Match check:
|
||||
// - exact folded char match always ok
|
||||
// - otherwise, allow equivalence only for word separators (e.g. '_' matches '-')
|
||||
var tCharFold = Unsafe.Add(ref tFold0, ti);
|
||||
if (qCharFold != tCharFold)
|
||||
{
|
||||
if (!skipWordSeparators)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (qCharKind != SymbolKind.WordSeparator ||
|
||||
SymbolClassifier.Classify(tCharFold) != SymbolKind.WordSeparator)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 0. Base char match bonus
|
||||
var score = charMatchBonus;
|
||||
|
||||
// 1. Consecutive match bonus
|
||||
if (seqLen > 0)
|
||||
{
|
||||
score += seqLen * consecutiveMultiplier;
|
||||
}
|
||||
|
||||
// 2. Same case bonus
|
||||
// Early outs to appease the branch predictor
|
||||
if (sameCaseBonus != 0)
|
||||
{
|
||||
var tCharRawCurr = Unsafe.Add(ref tRaw0, ti);
|
||||
var tCharRawCurrIsUpper = char.IsUpper(tCharRawCurr);
|
||||
if (qCharRawCurrIsUpper == tCharRawCurrIsUpper)
|
||||
{
|
||||
score += sameCaseBonus;
|
||||
}
|
||||
|
||||
if (ti == 0)
|
||||
{
|
||||
score += startOfWordBonus;
|
||||
return score;
|
||||
}
|
||||
|
||||
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
|
||||
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
|
||||
if (tPrevKind != SymbolKind.Other)
|
||||
{
|
||||
score += tPrevKind == SymbolKind.PathSeparator
|
||||
? pathSeparatorBonus
|
||||
: wordSeparatorBonus;
|
||||
|
||||
if (skipWordSeparators && seqLen == 0 && qi > 0)
|
||||
{
|
||||
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
|
||||
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
|
||||
|
||||
if (qPrevKind == SymbolKind.WordSeparator)
|
||||
{
|
||||
score += separatorAlignmentBonus;
|
||||
|
||||
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
|
||||
{
|
||||
score += exactSeparatorBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
if (tCharRawCurrIsUpper && seqLen == 0)
|
||||
{
|
||||
score += camelCaseBonus;
|
||||
return score;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ti == 0)
|
||||
{
|
||||
score += startOfWordBonus;
|
||||
return score;
|
||||
}
|
||||
|
||||
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
|
||||
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
|
||||
if (tPrevKind != SymbolKind.Other)
|
||||
{
|
||||
score += tPrevKind == SymbolKind.PathSeparator
|
||||
? pathSeparatorBonus
|
||||
: wordSeparatorBonus;
|
||||
|
||||
if (skipWordSeparators && seqLen == 0 && qi > 0)
|
||||
{
|
||||
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
|
||||
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
|
||||
|
||||
if (qPrevKind == SymbolKind.WordSeparator)
|
||||
{
|
||||
score += separatorAlignmentBonus;
|
||||
|
||||
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
|
||||
{
|
||||
score += exactSeparatorBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
if (camelCaseBonus != 0 && seqLen == 0 && char.IsUpper(Unsafe.Add(ref tRaw0, ti)))
|
||||
{
|
||||
score += camelCaseBonus;
|
||||
return score;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented is not null)
|
||||
{
|
||||
ArrayPool<int>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schema ID is for cache invalidation of precomputed targets.
|
||||
// Only includes options that affect folding/bloom, not scoring.
|
||||
private static uint ComputeSchemaId(PrecomputedFuzzyMatcherOptions o)
|
||||
{
|
||||
const uint fnvOffset = 2166136261;
|
||||
const uint fnvPrime = 16777619;
|
||||
|
||||
var h = fnvOffset;
|
||||
h = unchecked((h ^ FolderSchemaVersion) * fnvPrime);
|
||||
h = unchecked((h ^ BloomSchemaVersion) * fnvPrime);
|
||||
h = unchecked((h ^ (uint)(o.RemoveDiacritics ? 1 : 0)) * fnvPrime);
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class PrecomputedFuzzyMatcherOptions
|
||||
{
|
||||
public static PrecomputedFuzzyMatcherOptions Default { get; } = new();
|
||||
|
||||
/*
|
||||
* Bonuses
|
||||
*/
|
||||
public int CharMatchBonus { get; init; } = 1;
|
||||
|
||||
public int SameCaseBonus { get; init; } = 1;
|
||||
|
||||
public int ConsecutiveMultiplier { get; init; } = 5;
|
||||
|
||||
public int CamelCaseBonus { get; init; } = 2;
|
||||
|
||||
public int StartOfWordBonus { get; init; } = 8;
|
||||
|
||||
public int PathSeparatorBonus { get; init; } = 5;
|
||||
|
||||
public int WordSeparatorBonus { get; init; } = 4;
|
||||
|
||||
public int SeparatorAlignmentBonus { get; init; } = 2;
|
||||
|
||||
public int ExactSeparatorBonus { get; init; } = 1;
|
||||
|
||||
/*
|
||||
* Settings
|
||||
*/
|
||||
public bool RemoveDiacritics { get; init; } = true;
|
||||
|
||||
public bool SkipWordSeparators { get; init; } = true;
|
||||
|
||||
public bool IgnoreSameCaseBonusIfQueryIsAllLowercase { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ToolGood.Words.Pinyin;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class PrecomputedFuzzyMatcherWithPinyin : IPrecomputedFuzzyMatcher
|
||||
{
|
||||
private readonly IBloomFilter _bloom;
|
||||
private readonly PrecomputedFuzzyMatcher _core;
|
||||
|
||||
private readonly IStringFolder _stringFolder;
|
||||
private readonly PinyinFuzzyMatcherOptions _pinyin;
|
||||
|
||||
public PrecomputedFuzzyMatcherWithPinyin(
|
||||
PrecomputedFuzzyMatcherOptions coreOptions,
|
||||
PinyinFuzzyMatcherOptions pinyinOptions,
|
||||
IStringFolder stringFolder,
|
||||
IBloomFilter bloom)
|
||||
{
|
||||
_pinyin = pinyinOptions;
|
||||
_stringFolder = stringFolder;
|
||||
_bloom = bloom;
|
||||
|
||||
_core = new PrecomputedFuzzyMatcher(coreOptions, stringFolder, bloom);
|
||||
|
||||
SchemaId = CombineSchema(_core.SchemaId, _pinyin);
|
||||
}
|
||||
|
||||
public uint SchemaId { get; }
|
||||
|
||||
public FuzzyQuery PrecomputeQuery(string? input)
|
||||
{
|
||||
input ??= string.Empty;
|
||||
|
||||
var primary = _core.PrecomputeQuery(input);
|
||||
|
||||
// Fast exit if effectively off (provider should already filter, but keep robust)
|
||||
if (!IsPinyinEnabled(_pinyin))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
// Match legacy: remove apostrophes for query secondary
|
||||
var queryForPinyin = _pinyin.RemoveApostrophesForQuery ? RemoveApostrophesIfAny(input) : input;
|
||||
|
||||
var pinyin = WordsHelper.GetPinyin(queryForPinyin);
|
||||
if (string.IsNullOrEmpty(pinyin))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
var secondary = _core.PrecomputeQuery(pinyin);
|
||||
return new FuzzyQuery(
|
||||
primary.Original,
|
||||
primary.Folded,
|
||||
primary.Bloom,
|
||||
primary.EffectiveLength,
|
||||
primary.IsAllLowercaseAsciiOrNonLetter,
|
||||
secondary.Original,
|
||||
secondary.Folded,
|
||||
secondary.Bloom,
|
||||
secondary.EffectiveLength,
|
||||
secondary.SecondaryIsAllLowercaseAsciiOrNonLetter);
|
||||
}
|
||||
|
||||
public FuzzyTarget PrecomputeTarget(string? input)
|
||||
{
|
||||
input ??= string.Empty;
|
||||
|
||||
var primary = _core.PrecomputeTarget(input);
|
||||
|
||||
if (!IsPinyinEnabled(_pinyin))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
// Match legacy: only compute target pinyin when target contains Chinese
|
||||
if (!ContainsToolGoodChinese(input))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
var pinyin = WordsHelper.GetPinyin(input);
|
||||
if (string.IsNullOrEmpty(pinyin))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
var secondary = _core.PrecomputeTarget(pinyin);
|
||||
return new FuzzyTarget(
|
||||
primary.Original,
|
||||
primary.Folded,
|
||||
primary.Bloom,
|
||||
secondary.Original,
|
||||
secondary.Folded,
|
||||
secondary.Bloom);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target)
|
||||
=> _core.Score(in query, in target);
|
||||
|
||||
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o) => o.Mode switch
|
||||
{
|
||||
PinyinMode.Off => false,
|
||||
PinyinMode.On => true,
|
||||
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static bool IsSimplifiedChineseUi()
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ContainsToolGoodChinese(string s)
|
||||
{
|
||||
return WordsHelper.HasChinese(s);
|
||||
}
|
||||
|
||||
private static string RemoveApostrophesIfAny(string input)
|
||||
{
|
||||
var first = input.IndexOf('\'');
|
||||
if (first < 0)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var removeCount = 1;
|
||||
for (var i = first + 1; i < input.Length; i++)
|
||||
{
|
||||
if (input[i] == '\'')
|
||||
{
|
||||
removeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Create(input.Length - removeCount, input, static (dst, src) =>
|
||||
{
|
||||
var di = 0;
|
||||
for (var i = 0; i < src.Length; i++)
|
||||
{
|
||||
var c = src[i];
|
||||
if (c == '\'')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dst[di++] = c;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static uint CombineSchema(uint coreSchemaId, PinyinFuzzyMatcherOptions p)
|
||||
{
|
||||
const uint fnvOffset = 2166136261;
|
||||
const uint fnvPrime = 16777619;
|
||||
|
||||
var h = fnvOffset;
|
||||
h = unchecked((h ^ coreSchemaId) * fnvPrime);
|
||||
h = unchecked((h ^ (uint)p.Mode) * fnvPrime);
|
||||
h = unchecked((h ^ (p.RemoveApostrophesForQuery ? 1u : 0u)) * fnvPrime);
|
||||
|
||||
// bump if you change formatting/conversion behavior
|
||||
const uint pinyinAlgoVersion = 1;
|
||||
h = unchecked((h ^ pinyinAlgoVersion) * fnvPrime);
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
public sealed class StringFolder : IStringFolder
|
||||
{
|
||||
// Cache for diacritic-stripped uppercase characters.
|
||||
// Benign race: worst case is redundant computation writing the same value.
|
||||
// 0 = uncached, else cachedChar + 1
|
||||
private static readonly ushort[] StripCacheUpper = new ushort[char.MaxValue + 1];
|
||||
|
||||
public string Fold(string input, bool removeDiacritics)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!removeDiacritics || Ascii.IsValid(input))
|
||||
{
|
||||
if (IsAlreadyFoldedAndSlashNormalized(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return string.Create(input.Length, input, static (dst, src) =>
|
||||
{
|
||||
for (var i = 0; i < src.Length; i++)
|
||||
{
|
||||
var c = src[i];
|
||||
dst[i] = c == '\\' ? '/' : char.ToUpperInvariant(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return string.Create(input.Length, input, static (dst, src) =>
|
||||
{
|
||||
for (var i = 0; i < src.Length; i++)
|
||||
{
|
||||
var c = src[i];
|
||||
var upper = c == '\\' ? '/' : char.ToUpperInvariant(c);
|
||||
dst[i] = StripDiacriticsFromUpper(upper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static bool IsAlreadyFoldedAndSlashNormalized(string input)
|
||||
{
|
||||
var sawNonAscii = false;
|
||||
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var c = input[i];
|
||||
|
||||
if (c == '\\')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((uint)(c - 'a') <= 'z' - 'a')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c > 0x7F)
|
||||
{
|
||||
sawNonAscii = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sawNonAscii)
|
||||
{
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var c = input[i];
|
||||
if (c <= 0x7F)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
if (cat is UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static char StripDiacriticsFromUpper(char upper)
|
||||
{
|
||||
if (upper <= 0x7F)
|
||||
{
|
||||
return upper;
|
||||
}
|
||||
|
||||
// Never attempt normalization on lone UTF-16 surrogates.
|
||||
if (char.IsSurrogate(upper))
|
||||
{
|
||||
return upper;
|
||||
}
|
||||
|
||||
var cachedPlus1 = StripCacheUpper[upper];
|
||||
if (cachedPlus1 != 0)
|
||||
{
|
||||
return (char)(cachedPlus1 - 1);
|
||||
}
|
||||
|
||||
var mapped = StripDiacriticsSlow(upper);
|
||||
StripCacheUpper[upper] = (ushort)(mapped + 1);
|
||||
return mapped;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static char StripDiacriticsSlow(char upper)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseChar = FirstNonMark(upper, NormalizationForm.FormD);
|
||||
if (baseChar == '\0' || baseChar == upper)
|
||||
{
|
||||
var kd = FirstNonMark(upper, NormalizationForm.FormKD);
|
||||
if (kd != '\0')
|
||||
{
|
||||
baseChar = kd;
|
||||
}
|
||||
}
|
||||
|
||||
return char.ToUpperInvariant(baseChar == '\0' ? upper : baseChar);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Absolute safety: if globalization tables ever throw for some reason,
|
||||
// degrade gracefully rather than failing hard.
|
||||
return upper;
|
||||
}
|
||||
|
||||
static char FirstNonMark(char c, NormalizationForm form)
|
||||
{
|
||||
var normalized = c.ToString().Normalize(form);
|
||||
|
||||
foreach (var ch in normalized)
|
||||
{
|
||||
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||
if (cat is not (UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark or UnicodeCategory.EnclosingMark))
|
||||
{
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
|
||||
return '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||
|
||||
internal static class SymbolClassifier
|
||||
{
|
||||
// Embedded in .data section - no allocation, no static constructor
|
||||
private static ReadOnlySpan<byte> Lookup =>
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
|
||||
2, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 1, // 32-47: space=2, "=2, '=2, -=2, .=2, /=1
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, // 48-63: :=2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64-79
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, // 80-95: _=2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96-111
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 112-127
|
||||
];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SymbolKind Classify(char c)
|
||||
{
|
||||
return c > 0x7F ? SymbolKind.Other : (SymbolKind)Lookup[c];
|
||||
}
|
||||
}
|
||||
@@ -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.Core.Common.Text;
|
||||
|
||||
internal enum SymbolKind : byte
|
||||
{
|
||||
Other = 0,
|
||||
PathSeparator = 1,
|
||||
WordSeparator = 2,
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -13,7 +15,7 @@ using Windows.ApplicationModel.DataTransfer;
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
|
||||
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext, IPrecomputedListItem
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
@@ -22,6 +24,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||
|
||||
private FuzzyTargetCache _titleCache;
|
||||
private FuzzyTargetCache _subtitleCache;
|
||||
|
||||
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
|
||||
|
||||
protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized);
|
||||
@@ -116,6 +121,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
_itemTitle = model.Title;
|
||||
Subtitle = model.Subtitle;
|
||||
_titleCache.Invalidate();
|
||||
_subtitleCache.Invalidate();
|
||||
|
||||
Initialized |= InitializedState.FastInitialized;
|
||||
}
|
||||
@@ -249,6 +256,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Subtitle = "Item failed to load";
|
||||
MoreCommands = [];
|
||||
_icon = _errorIcon;
|
||||
_titleCache.Invalidate();
|
||||
_subtitleCache.Invalidate();
|
||||
Initialized |= InitializedState.Error;
|
||||
}
|
||||
|
||||
@@ -286,6 +295,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Subtitle = "Item failed to load";
|
||||
MoreCommands = [];
|
||||
_icon = _errorIcon;
|
||||
_titleCache.Invalidate();
|
||||
_subtitleCache.Invalidate();
|
||||
Initialized |= InitializedState.Error;
|
||||
}
|
||||
|
||||
@@ -335,12 +346,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
case nameof(Title):
|
||||
_itemTitle = model.Title;
|
||||
_titleCache.Invalidate();
|
||||
break;
|
||||
|
||||
case nameof(Subtitle):
|
||||
var modelSubtitle = model.Subtitle;
|
||||
this.Subtitle = modelSubtitle;
|
||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||
_subtitleCache.Invalidate();
|
||||
break;
|
||||
|
||||
case nameof(Icon):
|
||||
@@ -415,6 +428,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
||||
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||
_itemTitle = model.Title;
|
||||
_titleCache.Invalidate();
|
||||
UpdateProperty(nameof(Title), nameof(Name));
|
||||
|
||||
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
|
||||
@@ -436,6 +450,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
private void UpdateTitle(string? title)
|
||||
{
|
||||
_itemTitle = title ?? string.Empty;
|
||||
_titleCache.Invalidate();
|
||||
UpdateProperty(nameof(Title));
|
||||
}
|
||||
|
||||
@@ -456,6 +471,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(DataPackage));
|
||||
}
|
||||
|
||||
public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
|
||||
=> _titleCache.GetOrUpdate(matcher, Title);
|
||||
|
||||
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
|
||||
=> _subtitleCache.GetOrUpdate(matcher, Subtitle);
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
@@ -11,6 +11,8 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
public ExtensionObject<ICommand> Model { get; private set; } = new(null);
|
||||
|
||||
public bool IsSet => Model.Unsafe is not null;
|
||||
|
||||
protected bool IsInitialized { get; private set; }
|
||||
|
||||
protected bool IsFastInitialized { get; private set; }
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -16,6 +19,8 @@ namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
public partial class ContextMenuViewModel : ObservableObject,
|
||||
IRecipient<UpdateCommandBarMessage>
|
||||
{
|
||||
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||
|
||||
public ICommandBarContext? SelectedItem
|
||||
{
|
||||
get => field;
|
||||
@@ -39,8 +44,9 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
|
||||
private string _lastSearchText = string.Empty;
|
||||
|
||||
public ContextMenuViewModel()
|
||||
public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
{
|
||||
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
}
|
||||
|
||||
@@ -91,13 +97,14 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.Where(c => c.ShouldBeVisible);
|
||||
|
||||
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
|
||||
var query = _fuzzyMatcherProvider.Current.PrecomputeQuery(searchText);
|
||||
var newResults = InternalListHelpers.FilterList(commands, in query, ScoreFunction);
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
|
||||
}
|
||||
|
||||
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
|
||||
private int ScoreFunction(in FuzzyQuery query, CommandContextItemViewModel item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
|
||||
if (string.IsNullOrWhiteSpace(query.Original))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@@ -107,11 +114,21 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
return 0;
|
||||
}
|
||||
|
||||
var nameMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Title);
|
||||
var fuzzyMatcher = _fuzzyMatcherProvider.Current;
|
||||
var title = item.GetTitleTarget(fuzzyMatcher);
|
||||
var subtitle = item.GetSubtitleTarget(fuzzyMatcher);
|
||||
|
||||
var descriptionMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Subtitle);
|
||||
var titleScore = fuzzyMatcher.Score(query, title);
|
||||
var subtitleScore = (fuzzyMatcher.Score(query, subtitle) - 4) / 2;
|
||||
|
||||
return new[] { nameMatch, (descriptionMatch - 4) / 2, 0 }.Max();
|
||||
return Max3(titleScore, subtitleScore, 0);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Max3(int a, int b, int c)
|
||||
{
|
||||
var m = a > b ? a : b;
|
||||
return m > c ? m : c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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.Core.ViewModels;
|
||||
|
||||
public enum ListItemType
|
||||
{
|
||||
Item,
|
||||
SectionHeader,
|
||||
Separator,
|
||||
}
|
||||
@@ -24,7 +24,9 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
|
||||
public string Section { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsSectionOrSeparator { get; private set; }
|
||||
public ListItemType Type { get; private set; }
|
||||
|
||||
public bool IsInteractive => Type == ListItemType.Item;
|
||||
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
@@ -85,15 +87,17 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
|
||||
UpdateTags(li.Tags);
|
||||
Section = li.Section ?? string.Empty;
|
||||
IsSectionOrSeparator = IsSeparator(li);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
Type = EvaluateType();
|
||||
UpdateProperty(nameof(Section), nameof(Type), nameof(IsInteractive));
|
||||
|
||||
UpdateAccessibleName();
|
||||
}
|
||||
|
||||
private bool IsSeparator(IListItem item)
|
||||
private ListItemType EvaluateType()
|
||||
{
|
||||
return item.Command is null;
|
||||
return Command.IsSet
|
||||
? ListItemType.Item
|
||||
: string.IsNullOrEmpty(Section) ? ListItemType.Separator : ListItemType.SectionHeader;
|
||||
}
|
||||
|
||||
public override void SlowInitializeProperties()
|
||||
@@ -140,12 +144,12 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
break;
|
||||
case nameof(model.Section):
|
||||
Section = model.Section ?? string.Empty;
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
Type = EvaluateType();
|
||||
UpdateProperty(nameof(Section), nameof(Type), nameof(IsInteractive));
|
||||
break;
|
||||
case nameof(model.Command):
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(IsSectionOrSeparator));
|
||||
Type = EvaluateType();
|
||||
UpdateProperty(nameof(Type), nameof(IsInteractive));
|
||||
break;
|
||||
case nameof(Details):
|
||||
var extensionDetails = model.Details;
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
|
||||
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
@@ -128,10 +130,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsColorIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsImageTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
OnPropertyChanged(nameof(IsResetButtonVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
@@ -179,6 +184,19 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
{
|
||||
_settings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageTintIntensity
|
||||
{
|
||||
get => _settings.BackgroundImageTintIntensity;
|
||||
set
|
||||
{
|
||||
_settings.BackgroundImageTintIntensity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
@@ -279,12 +297,108 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
};
|
||||
}
|
||||
|
||||
public int BackdropOpacity
|
||||
{
|
||||
get => _settings.BackdropOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackdropOpacity != value)
|
||||
{
|
||||
_settings.BackdropOpacity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
||||
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropStyleIndex
|
||||
{
|
||||
get => (int)_settings.BackdropStyle;
|
||||
set
|
||||
{
|
||||
var newStyle = (BackdropStyle)value;
|
||||
if (_settings.BackdropStyle != newStyle)
|
||||
{
|
||||
_settings.BackdropStyle = newStyle;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
|
||||
OnPropertyChanged(nameof(IsMicaBackdropDescriptionVisible));
|
||||
OnPropertyChanged(nameof(IsBackgroundSettingsEnabled));
|
||||
OnPropertyChanged(nameof(IsBackgroundNotAvailableVisible));
|
||||
|
||||
if (!IsBackgroundSettingsEnabled)
|
||||
{
|
||||
IsColorizationDetailsExpanded = false;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop opacity slider should be visible.
|
||||
/// </summary>
|
||||
public bool IsBackdropOpacityVisible =>
|
||||
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop description (for styles without options) should be visible.
|
||||
/// </summary>
|
||||
public bool IsMicaBackdropDescriptionVisible =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether background/colorization settings are available.
|
||||
/// </summary>
|
||||
public bool IsBackgroundSettingsEnabled =>
|
||||
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
|
||||
/// </summary>
|
||||
public bool IsBackgroundNotAvailableVisible =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
public BackdropStyle? EffectiveBackdropStyle
|
||||
{
|
||||
get
|
||||
{
|
||||
// Return style when transparency/blur is visible (not fully opaque Acrylic)
|
||||
// - Clear/Mica/MicaAlt/AcrylicThin always show their effect
|
||||
// - Acrylic shows effect only when opacity < 100
|
||||
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
|
||||
{
|
||||
return _settings.BackdropStyle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public double EffectiveImageOpacity =>
|
||||
EffectiveBackdropStyle is not null
|
||||
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
|
||||
: (BackgroundImageOpacity / 100f);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
|
||||
/// </summary>
|
||||
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
|
||||
? _settings.BackgroundImageTintIntensity
|
||||
: _settings.CustomThemeColorIntensity;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
@@ -292,16 +406,21 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
public Color EffectiveThemeColor =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
|
||||
? Colors.Transparent
|
||||
: ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
|
||||
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
|
||||
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||
@@ -309,11 +428,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
|
||||
? null
|
||||
: ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
@@ -327,7 +448,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
@@ -357,6 +478,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
// Theme services recalculates effective color and opacity based on current settings.
|
||||
EffectiveBackdrop = _themeService.Current.BackdropParameters;
|
||||
OnPropertyChanged(nameof(EffectiveBackdrop));
|
||||
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
||||
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||
@@ -379,7 +502,28 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
BackgroundImageTintIntensity = 0;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetAppearanceSettings()
|
||||
{
|
||||
// Reset theme
|
||||
Theme = UserTheme.Default;
|
||||
|
||||
// Reset backdrop settings
|
||||
BackdropStyleIndex = (int)BackdropStyle.Acrylic;
|
||||
BackdropOpacity = 100;
|
||||
|
||||
// Reset background image settings
|
||||
BackgroundImagePath = string.Empty;
|
||||
ResetBackgroundImageProperties();
|
||||
|
||||
// Reset colorization
|
||||
ColorizationMode = ColorizationMode.None;
|
||||
ThemeColor = DefaultTintColor;
|
||||
ColorIntensity = 100;
|
||||
BackgroundImageTintIntensity = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of system backdrop controller to use.
|
||||
/// </summary>
|
||||
public enum BackdropControllerKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Solid color with alpha transparency (TransparentTintBackdrop).
|
||||
/// </summary>
|
||||
Solid,
|
||||
|
||||
/// <summary>
|
||||
/// Desktop Acrylic with default blur (DesktopAcrylicKind.Default).
|
||||
/// </summary>
|
||||
Acrylic,
|
||||
|
||||
/// <summary>
|
||||
/// Desktop Acrylic with thinner blur (DesktopAcrylicKind.Thin).
|
||||
/// </summary>
|
||||
AcrylicThin,
|
||||
|
||||
/// <summary>
|
||||
/// Mica effect (MicaKind.Base).
|
||||
/// </summary>
|
||||
Mica,
|
||||
|
||||
/// <summary>
|
||||
/// Mica alternate/darker variant (MicaKind.BaseAlt).
|
||||
/// </summary>
|
||||
MicaAlt,
|
||||
|
||||
/// <summary>
|
||||
/// Custom backdrop implementation.
|
||||
/// </summary>
|
||||
Custom,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the visual backdrop style for the window.
|
||||
/// </summary>
|
||||
public enum BackdropStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard desktop acrylic with blur effect.
|
||||
/// </summary>
|
||||
Acrylic,
|
||||
|
||||
/// <summary>
|
||||
/// Solid color with alpha transparency (no blur).
|
||||
/// </summary>
|
||||
Clear,
|
||||
|
||||
/// <summary>
|
||||
/// Mica effect that samples the desktop wallpaper.
|
||||
/// </summary>
|
||||
Mica,
|
||||
|
||||
/// <summary>
|
||||
/// Thinner acrylic variant with more transparency.
|
||||
/// </summary>
|
||||
AcrylicThin,
|
||||
|
||||
/// <summary>
|
||||
/// Mica alternate variant (darker).
|
||||
/// </summary>
|
||||
MicaAlt,
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration parameters for a backdrop style.
|
||||
/// </summary>
|
||||
public sealed record BackdropStyleConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type of system backdrop controller to use.
|
||||
/// </summary>
|
||||
public required BackdropControllerKind ControllerKind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base tint opacity before user adjustments.
|
||||
/// </summary>
|
||||
public required float BaseTintOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base luminosity opacity before user adjustments.
|
||||
/// </summary>
|
||||
public required float BaseLuminosityOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush type to use for preview approximation.
|
||||
/// </summary>
|
||||
public required PreviewBrushKind PreviewBrush { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fixed opacity for styles that don't support user adjustment (e.g., Mica).
|
||||
/// When <see cref="SupportsOpacity"/> is false, this value is used as the effective opacity.
|
||||
/// </summary>
|
||||
public float FixedOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this backdrop style supports custom colorization (tint colors).
|
||||
/// </summary>
|
||||
public bool SupportsColorization { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this backdrop style supports custom background images.
|
||||
/// </summary>
|
||||
public bool SupportsBackgroundImage { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this backdrop style supports opacity adjustment.
|
||||
/// </summary>
|
||||
public bool SupportsOpacity { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the effective tint opacity based on this style's configuration.
|
||||
/// </summary>
|
||||
/// <param name="userOpacity">User's backdrop opacity setting (0-1 normalized).</param>
|
||||
/// <param name="baseTintOpacityOverride">Optional override for base tint opacity (used by colorful theme).</param>
|
||||
/// <returns>The effective opacity to apply.</returns>
|
||||
public float ComputeEffectiveOpacity(float userOpacity, float? baseTintOpacityOverride = null)
|
||||
{
|
||||
// For styles that don't support opacity (Mica), use FixedOpacity
|
||||
if (!SupportsOpacity && FixedOpacity > 0)
|
||||
{
|
||||
return FixedOpacity;
|
||||
}
|
||||
|
||||
// For Solid: only user opacity matters (controls alpha of solid color)
|
||||
if (ControllerKind == BackdropControllerKind.Solid)
|
||||
{
|
||||
return userOpacity;
|
||||
}
|
||||
|
||||
// For blur effects: multiply base opacity with user opacity
|
||||
var baseTint = baseTintOpacityOverride ?? BaseTintOpacity;
|
||||
return baseTint * userOpacity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Central registry of backdrop style configurations.
|
||||
/// </summary>
|
||||
public static class BackdropStyles
|
||||
{
|
||||
private static readonly Dictionary<BackdropStyle, BackdropStyleConfig> Configs = new()
|
||||
{
|
||||
[BackdropStyle.Acrylic] = new()
|
||||
{
|
||||
ControllerKind = BackdropControllerKind.Acrylic,
|
||||
BaseTintOpacity = 0.5f,
|
||||
BaseLuminosityOpacity = 0.9f,
|
||||
PreviewBrush = PreviewBrushKind.Acrylic,
|
||||
},
|
||||
[BackdropStyle.AcrylicThin] = new()
|
||||
{
|
||||
ControllerKind = BackdropControllerKind.AcrylicThin,
|
||||
BaseTintOpacity = 0.0f,
|
||||
BaseLuminosityOpacity = 0.85f,
|
||||
PreviewBrush = PreviewBrushKind.Acrylic,
|
||||
},
|
||||
[BackdropStyle.Mica] = new()
|
||||
{
|
||||
ControllerKind = BackdropControllerKind.Mica,
|
||||
BaseTintOpacity = 0.0f,
|
||||
BaseLuminosityOpacity = 1.0f,
|
||||
PreviewBrush = PreviewBrushKind.Solid,
|
||||
FixedOpacity = 0.96f,
|
||||
SupportsOpacity = false,
|
||||
},
|
||||
[BackdropStyle.MicaAlt] = new()
|
||||
{
|
||||
ControllerKind = BackdropControllerKind.MicaAlt,
|
||||
BaseTintOpacity = 0.0f,
|
||||
BaseLuminosityOpacity = 1.0f,
|
||||
PreviewBrush = PreviewBrushKind.Solid,
|
||||
FixedOpacity = 0.98f,
|
||||
SupportsOpacity = false,
|
||||
},
|
||||
[BackdropStyle.Clear] = new()
|
||||
{
|
||||
ControllerKind = BackdropControllerKind.Solid,
|
||||
BaseTintOpacity = 1.0f,
|
||||
BaseLuminosityOpacity = 1.0f,
|
||||
PreviewBrush = PreviewBrushKind.Solid,
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration for the specified backdrop style.
|
||||
/// </summary>
|
||||
public static BackdropStyleConfig Get(BackdropStyle style) =>
|
||||
Configs.TryGetValue(style, out var config) ? config : Configs[BackdropStyle.Acrylic];
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered backdrop styles.
|
||||
/// </summary>
|
||||
public static IEnumerable<BackdropStyle> All => Configs.Keys;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
@@ -24,7 +25,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
/// This class encapsulates the data we load from built-in providers and extensions to use within the same extension-UI system for a <see cref="ListPage"/>.
|
||||
/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels.
|
||||
/// </summary>
|
||||
public partial class MainListPage : DynamicListPage,
|
||||
public sealed partial class MainListPage : DynamicListPage,
|
||||
IRecipient<ClearSearchMessage>,
|
||||
IRecipient<UpdateFallbackItemsMessage>, IDisposable
|
||||
{
|
||||
@@ -32,13 +33,18 @@ public partial class MainListPage : DynamicListPage,
|
||||
private readonly AliasManager _aliasManager;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly AppStateModel _appStateModel;
|
||||
private List<Scored<IListItem>>? _filteredItems;
|
||||
private List<Scored<IListItem>>? _filteredApps;
|
||||
private readonly ScoringFunction<IListItem> _scoringFunction;
|
||||
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
|
||||
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||
|
||||
private RoScored<IListItem>[]? _filteredItems;
|
||||
private RoScored<IListItem>[]? _filteredApps;
|
||||
|
||||
// Keep as IEnumerable for deferred execution. Fallback item titles are updated
|
||||
// asynchronously, so scoring must happen lazily when GetItems is called.
|
||||
private IEnumerable<Scored<IListItem>>? _scoredFallbackItems;
|
||||
private IEnumerable<Scored<IListItem>>? _fallbackItems;
|
||||
private IEnumerable<RoScored<IListItem>>? _scoredFallbackItems;
|
||||
private IEnumerable<RoScored<IListItem>>? _fallbackItems;
|
||||
|
||||
private bool _includeApps;
|
||||
private bool _filteredItemsIncludesApps;
|
||||
private int _appResultLimit = 10;
|
||||
@@ -48,7 +54,12 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
||||
public MainListPage(
|
||||
TopLevelCommandManager topLevelCommandManager,
|
||||
SettingsModel settings,
|
||||
AliasManager aliasManager,
|
||||
AppStateModel appStateModel,
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
{
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
@@ -58,6 +69,10 @@ public partial class MainListPage : DynamicListPage,
|
||||
_aliasManager = aliasManager;
|
||||
_appStateModel = appStateModel;
|
||||
_tlcManager = topLevelCommandManager;
|
||||
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
||||
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
|
||||
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
|
||||
|
||||
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
|
||||
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
||||
|
||||
@@ -190,8 +205,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
@@ -354,15 +368,14 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
if (_includeApps)
|
||||
{
|
||||
var allNewApps = AllAppsCommandProvider.Page.GetItems().ToList();
|
||||
var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast<AppListItem>().ToList();
|
||||
|
||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
||||
|
||||
if (pinnedApps.Length > 0)
|
||||
{
|
||||
newApps = allNewApps.Where(w =>
|
||||
pinnedApps.IndexOf(((AppListItem)w).AppIdentifier) < 0);
|
||||
newApps = allNewApps.Where(w => pinnedApps.IndexOf(w.AppIdentifier) < 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -376,11 +389,10 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
var history = _appStateModel.RecentCommands!;
|
||||
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
|
||||
var searchQuery = _fuzzyMatcherProvider.Current.PrecomputeQuery(SearchText);
|
||||
|
||||
// Produce a list of everything that matches the current filter.
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
|
||||
_filteredItems = InternalListHelpers.FilterListWithScores(newFilteredItems, searchQuery, _scoringFunction);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -388,21 +400,14 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
|
||||
_scoredFallbackItems = InternalListHelpers.FilterListWithScores(newFallbacksForScoring, searchQuery, _scoringFunction);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scoredFallbackItems = ListHelpers.FilterListWithScores<IListItem>(newFallbacksForScoring ?? [], SearchText, scoreItem);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Func<string, IListItem, int> scoreFallbackItem = (a, b) => { return ScoreFallbackItem(a, b, _settings.FallbackRanks); };
|
||||
_fallbackItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFallbacks ?? [], SearchText, scoreFallbackItem)];
|
||||
_fallbackItems = InternalListHelpers.FilterListWithScores<IListItem>(newFallbacks ?? [], searchQuery, _fallbackScoringFunction);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -412,18 +417,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Produce a list of filtered apps with the appropriate limit
|
||||
if (newApps.Any())
|
||||
{
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We'll apply this limit in the GetItems method after merging with commands
|
||||
// but we need to know the limit now to avoid re-scoring apps
|
||||
var appLimit = AllAppsCommandProvider.TopLevelResultLimit;
|
||||
|
||||
_filteredApps = [.. scoredApps];
|
||||
_filteredApps = InternalListHelpers.FilterListWithScores(newApps, searchQuery, _scoringFunction);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -431,10 +425,15 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
var filterDoneTimestamp = stopwatch.ElapsedMilliseconds;
|
||||
Logger.LogDebug($"Filter with '{newSearch}' in {filterDoneTimestamp}ms");
|
||||
|
||||
RaiseItemsChanged();
|
||||
|
||||
timer.Stop();
|
||||
Logger.LogDebug($"Filter with '{newSearch}' in {timer.ElapsedMilliseconds}ms");
|
||||
var listPageUpdatedTimestamp = stopwatch.ElapsedMilliseconds;
|
||||
Logger.LogDebug($"Render items with '{newSearch}' in {listPageUpdatedTimestamp}ms /d {listPageUpdatedTimestamp - filterDoneTimestamp}ms");
|
||||
|
||||
stopwatch.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +477,11 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
||||
// fact that we want fallback handlers down-weighted, so that they don't
|
||||
// _always_ show up first.
|
||||
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
|
||||
internal static int ScoreTopLevelItem(
|
||||
in FuzzyQuery query,
|
||||
IListItem topLevelOrAppItem,
|
||||
IRecentCommandsManager history,
|
||||
IPrecomputedFuzzyMatcher precomputedFuzzyMatcher)
|
||||
{
|
||||
var title = topLevelOrAppItem.Title;
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -486,94 +489,80 @@ public partial class MainListPage : DynamicListPage,
|
||||
return 0;
|
||||
}
|
||||
|
||||
var isWhiteSpace = string.IsNullOrWhiteSpace(query);
|
||||
|
||||
var isFallback = false;
|
||||
var isAliasSubstringMatch = false;
|
||||
var isAliasMatch = false;
|
||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||
|
||||
var extensionDisplayName = string.Empty;
|
||||
FuzzyTarget? extensionDisplayNameTarget = null;
|
||||
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
isFallback = topLevel.IsFallback;
|
||||
extensionDisplayNameTarget = topLevel.GetExtensionNameTarget(precomputedFuzzyMatcher);
|
||||
|
||||
if (topLevel.HasAlias)
|
||||
{
|
||||
var alias = topLevel.AliasText;
|
||||
isAliasMatch = alias == query;
|
||||
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query, StringComparison.CurrentCultureIgnoreCase);
|
||||
isAliasMatch = alias == query.Original;
|
||||
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query.Original, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
extensionDisplayName = topLevel.ExtensionHost?.Extension?.PackageDisplayName ?? string.Empty;
|
||||
}
|
||||
|
||||
// StringMatcher.FuzzySearch will absolutely BEEF IT if you give it a
|
||||
// whitespace-only query.
|
||||
//
|
||||
// in that scenario, we'll just use a simple string contains for the
|
||||
// query. Maybe someone is really looking for things with a space in
|
||||
// them, I don't know.
|
||||
|
||||
// Title:
|
||||
// * whitespace query: 1 point
|
||||
// * otherwise full weight match
|
||||
var nameMatch = isWhiteSpace ?
|
||||
(title.Contains(query) ? 1 : 0) :
|
||||
FuzzyStringMatcher.ScoreFuzzy(query, title);
|
||||
|
||||
// Subtitle:
|
||||
// * whitespace query: 1/2 point
|
||||
// * otherwise ~half weight match. Minus a bit, because subtitles tend to be longer
|
||||
var descriptionMatch = isWhiteSpace ?
|
||||
(topLevelOrAppItem.Subtitle.Contains(query) ? .5 : 0) :
|
||||
(FuzzyStringMatcher.ScoreFuzzy(query, topLevelOrAppItem.Subtitle) - 4) / 2.0;
|
||||
|
||||
// Extension title: despite not being visible, give the extension name itself some weight
|
||||
// * whitespace query: 0 points
|
||||
// * otherwise more weight than a subtitle, but not much
|
||||
var extensionTitleMatch = isWhiteSpace ? 0 : FuzzyStringMatcher.ScoreFuzzy(query, extensionDisplayName) / 1.5;
|
||||
|
||||
var scores = new[]
|
||||
// Handle whitespace query separately - FuzzySearch doesn't handle it well
|
||||
if (string.IsNullOrWhiteSpace(query.Original))
|
||||
{
|
||||
nameMatch,
|
||||
descriptionMatch,
|
||||
isFallback ? 1 : 0, // Always give fallbacks a chance
|
||||
};
|
||||
var max = scores.Max();
|
||||
return ScoreWhitespaceQuery(query.Original, title, topLevelOrAppItem.Subtitle, isFallback);
|
||||
}
|
||||
|
||||
// _Add_ the extension name. This will bubble items that match both
|
||||
// title and extension name up above ones that just match title.
|
||||
// e.g. "git" will up-weight "GitHub searches" from the GitHub extension
|
||||
// above "git" from "whatever"
|
||||
max = max + extensionTitleMatch;
|
||||
// Get precomputed targets
|
||||
var (titleTarget, subtitleTarget) = topLevelOrAppItem is IPrecomputedListItem precomputedItem
|
||||
? (precomputedItem.GetTitleTarget(precomputedFuzzyMatcher), precomputedItem.GetSubtitleTarget(precomputedFuzzyMatcher))
|
||||
: (precomputedFuzzyMatcher.PrecomputeTarget(title), precomputedFuzzyMatcher.PrecomputeTarget(topLevelOrAppItem.Subtitle));
|
||||
|
||||
// Score components
|
||||
var nameScore = precomputedFuzzyMatcher.Score(query, titleTarget);
|
||||
var descriptionScore = (precomputedFuzzyMatcher.Score(query, subtitleTarget) - 4) / 2.0;
|
||||
var extensionScore = extensionDisplayNameTarget is { } extTarget ? precomputedFuzzyMatcher.Score(query, extTarget) / 1.5 : 0;
|
||||
|
||||
// Take best match from title/description/fallback, then add extension score
|
||||
// Extension adds to max so items matching both title AND extension bubble up
|
||||
var baseScore = Math.Max(Math.Max(nameScore, descriptionScore), isFallback ? 1 : 0);
|
||||
var matchScore = baseScore + extensionScore;
|
||||
|
||||
// Apply a penalty to fallback items so they rank below direct matches.
|
||||
// Fallbacks that dynamically match queries (like RDP connections) should
|
||||
// appear after apps and direct command matches.
|
||||
if (isFallback && max > 1)
|
||||
if (isFallback && matchScore > 1)
|
||||
{
|
||||
// Reduce fallback scores by 50% to prioritize direct matches
|
||||
max = max * 0.5;
|
||||
matchScore = matchScore * 0.5;
|
||||
}
|
||||
|
||||
var matchSomething = max
|
||||
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
|
||||
// Alias matching: exact match is overwhelming priority, substring match adds a small boost
|
||||
var aliasBoost = isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0);
|
||||
var totalMatch = matchScore + aliasBoost;
|
||||
|
||||
// If we matched title, subtitle, or alias (something real), then
|
||||
// here we add the recent command weight boost
|
||||
//
|
||||
// Otherwise something like `x` will still match everything you've run before
|
||||
var finalScore = matchSomething * 10;
|
||||
if (matchSomething > 0)
|
||||
// Apply scaling and history boost only if we matched something real
|
||||
var finalScore = totalMatch * 10;
|
||||
if (totalMatch > 0)
|
||||
{
|
||||
var recentWeightBoost = history.GetCommandHistoryWeight(id);
|
||||
finalScore += recentWeightBoost;
|
||||
finalScore += history.GetCommandHistoryWeight(id);
|
||||
}
|
||||
|
||||
return (int)finalScore;
|
||||
}
|
||||
|
||||
internal static int ScoreFallbackItem(string query, IListItem topLevelOrAppItem, string[] fallbackRanks)
|
||||
private static int ScoreWhitespaceQuery(string query, string title, string subtitle, bool isFallback)
|
||||
{
|
||||
// Simple contains check for whitespace queries
|
||||
var nameMatch = title.Contains(query, StringComparison.Ordinal) ? 1.0 : 0;
|
||||
var descriptionMatch = subtitle.Contains(query, StringComparison.Ordinal) ? 0.5 : 0;
|
||||
var baseScore = Math.Max(Math.Max(nameMatch, descriptionMatch), isFallback ? 1 : 0);
|
||||
|
||||
return (int)(baseScore * 10);
|
||||
}
|
||||
|
||||
private static int ScoreFallbackItem(IListItem topLevelOrAppItem, string[] fallbackRanks)
|
||||
{
|
||||
// Default to 1 so it always shows in list.
|
||||
var finalScore = 1;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#pragma warning disable IDE0007 // Use implicit type
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -16,10 +17,10 @@ internal static class MainListPageResultFactory
|
||||
/// applying an application result limit and filtering fallback items as needed.
|
||||
/// </summary>
|
||||
public static IListItem[] Create(
|
||||
IList<Scored<IListItem>>? filteredItems,
|
||||
IList<Scored<IListItem>>? scoredFallbackItems,
|
||||
IList<Scored<IListItem>>? filteredApps,
|
||||
IList<Scored<IListItem>>? fallbackItems,
|
||||
IList<RoScored<IListItem>>? filteredItems,
|
||||
IList<RoScored<IListItem>>? scoredFallbackItems,
|
||||
IList<RoScored<IListItem>>? filteredApps,
|
||||
IList<RoScored<IListItem>>? fallbackItems,
|
||||
int appResultLimit)
|
||||
{
|
||||
if (appResultLimit < 0)
|
||||
@@ -147,7 +148,7 @@ internal static class MainListPageResultFactory
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int GetNonEmptyFallbackItemsCount(IList<Scored<IListItem>>? fallbackItems)
|
||||
private static int GetNonEmptyFallbackItemsCount(IList<RoScored<IListItem>>? fallbackItems)
|
||||
{
|
||||
int fallbackItemsCount = 0;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
|
||||
public partial double BackgroundImageOpacity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -39,6 +40,30 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
[ObservableProperty]
|
||||
public partial bool ShowBackgroundImage { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(EffectiveBackdropStyle))]
|
||||
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
|
||||
public partial BackdropStyle BackdropStyle { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(EffectiveBackdropStyle))]
|
||||
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
|
||||
public partial float BackdropOpacity { get; private set; } = 1.0f;
|
||||
|
||||
// Returns null when no transparency needed (BlurImageControl uses this to decide source type)
|
||||
public BackdropStyle? EffectiveBackdropStyle =>
|
||||
BackdropStyle == BackdropStyle.Clear ||
|
||||
BackdropStyle == BackdropStyle.Mica ||
|
||||
BackdropOpacity < 1.0f
|
||||
? BackdropStyle
|
||||
: null;
|
||||
|
||||
// When transparency is enabled, use square root curve so image stays visible longer as backdrop fades
|
||||
public double EffectiveImageOpacity =>
|
||||
EffectiveBackdropStyle is not null
|
||||
? BackgroundImageOpacity * Math.Sqrt(BackdropOpacity)
|
||||
: BackgroundImageOpacity;
|
||||
|
||||
public MainWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
@@ -58,6 +83,9 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
|
||||
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
|
||||
|
||||
BackdropStyle = _themeService.Current.BackdropParameters.Style;
|
||||
BackdropOpacity = _themeService.Current.BackdropOpacity;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the brush type to use for backdrop preview approximation.
|
||||
/// </summary>
|
||||
public enum PreviewBrushKind
|
||||
{
|
||||
/// <summary>
|
||||
/// SolidColorBrush with computed alpha.
|
||||
/// </summary>
|
||||
Solid,
|
||||
|
||||
/// <summary>
|
||||
/// AcrylicBrush with blur effect.
|
||||
/// </summary>
|
||||
Acrylic,
|
||||
}
|
||||
@@ -1,9 +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 Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for configuring the window backdrop appearance.
|
||||
/// </summary>
|
||||
/// <param name="TintColor">The tint color applied to the backdrop.</param>
|
||||
/// <param name="FallbackColor">The fallback color when backdrop effects are unavailable.</param>
|
||||
/// <param name="EffectiveOpacity">
|
||||
/// The effective opacity for the backdrop, pre-computed by the theme provider.
|
||||
/// For Acrylic style: TintOpacity * BackdropOpacity.
|
||||
/// For Clear style: BackdropOpacity (controls the solid color alpha).
|
||||
/// </param>
|
||||
/// <param name="EffectiveLuminosityOpacity">
|
||||
/// The effective luminosity opacity for Acrylic backdrop, pre-computed by the theme provider.
|
||||
/// Computed as LuminosityOpacity * BackdropOpacity.
|
||||
/// </param>
|
||||
/// <param name="Style">The backdrop style (Acrylic or Clear).</param>
|
||||
public sealed record BackdropParameters(
|
||||
Color TintColor,
|
||||
Color FallbackColor,
|
||||
float EffectiveOpacity,
|
||||
float EffectiveLuminosityOpacity,
|
||||
BackdropStyle Style = BackdropStyle.Acrylic);
|
||||
@@ -51,12 +51,23 @@ public sealed class ThemeSnapshot
|
||||
public required double BackgroundImageOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
||||
/// Gets the effective backdrop parameters based on current settings and theme.
|
||||
/// </summary>
|
||||
/// <returns>The resolved <c>AcrylicBackdropParameters</c> to apply.</returns>
|
||||
public required AcrylicBackdropParameters BackdropParameters { get; init; }
|
||||
/// <returns>The resolved <c>BackdropParameters</c> to apply.</returns>
|
||||
public required BackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw backdrop opacity setting (0-1 range).
|
||||
/// Used for determining if transparency is enabled and for image opacity calculations.
|
||||
/// </summary>
|
||||
public required float BackdropOpacity { get; init; }
|
||||
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether colorization is active (accent color, custom color, or image mode).
|
||||
/// </summary>
|
||||
public required bool HasColorization { get; init; }
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
public int BackgroundImageTintIntensity { get; set; }
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
@@ -84,6 +86,10 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
public BackdropStyle BackdropStyle { get; set; }
|
||||
|
||||
public int BackdropOpacity { get; set; } = 100;
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
@@ -16,7 +19,8 @@ using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
|
||||
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider, IPrecomputedListItem
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
@@ -34,6 +38,10 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
private HotkeySettings? _hotkey;
|
||||
private IIconInfo? _initialIcon;
|
||||
|
||||
private FuzzyTargetCache _titleCache;
|
||||
private FuzzyTargetCache _subtitleCache;
|
||||
private FuzzyTargetCache _extensionNameCache;
|
||||
|
||||
private CommandAlias? Alias { get; set; }
|
||||
|
||||
public bool IsFallback { get; private set; }
|
||||
@@ -176,6 +184,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
}
|
||||
}
|
||||
|
||||
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
|
||||
|
||||
public TopLevelViewModel(
|
||||
CommandItemViewModel item,
|
||||
bool isFallback,
|
||||
@@ -230,6 +240,15 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));
|
||||
|
||||
if (e.PropertyName is nameof(CommandItemViewModel.Title) or nameof(CommandItemViewModel.Name))
|
||||
{
|
||||
_titleCache.Invalidate();
|
||||
}
|
||||
else if (e.PropertyName is nameof(CommandItemViewModel.Subtitle))
|
||||
{
|
||||
_subtitleCache.Invalidate();
|
||||
}
|
||||
|
||||
if (e.PropertyName is "IsInitialized" or nameof(CommandItemViewModel.Command))
|
||||
{
|
||||
GenerateId();
|
||||
@@ -420,4 +439,18 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
|
||||
public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
|
||||
=> _titleCache.GetOrUpdate(matcher, Title);
|
||||
|
||||
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
|
||||
=> _subtitleCache.GetOrUpdate(matcher, Subtitle);
|
||||
|
||||
public FuzzyTarget GetExtensionNameTarget(IPrecomputedFuzzyMatcher matcher)
|
||||
=> _extensionNameCache.GetOrUpdate(matcher, ExtensionName);
|
||||
|
||||
private string GetDebuggerDisplay()
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop;
|
||||
using Microsoft.CmdPal.Ext.Shell;
|
||||
@@ -67,11 +69,13 @@ public partial class App : Application, IDisposable
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
var appInfoService = new ApplicationInfoService();
|
||||
|
||||
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
|
||||
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default);
|
||||
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default, appInfoService);
|
||||
#endif
|
||||
|
||||
Services = ConfigureServices();
|
||||
Services = ConfigureServices(appInfoService);
|
||||
|
||||
IconCacheProvider.Initialize(Services);
|
||||
|
||||
@@ -92,6 +96,9 @@ public partial class App : Application, IDisposable
|
||||
// This way, log statements from the core project will be captured by the PT logs
|
||||
var logWrapper = new LogWrapper();
|
||||
CoreLogger.InitializeLogger(logWrapper);
|
||||
|
||||
// Now that CoreLogger is initialized, initialize the logger delegate in ApplicationInfoService
|
||||
appInfoService.SetLogDirectory(() => Logger.CurrentVersionLogDirectoryPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,7 +116,7 @@ public partial class App : Application, IDisposable
|
||||
/// <summary>
|
||||
/// Configures the services for the application
|
||||
/// </summary>
|
||||
private static ServiceProvider ConfigureServices()
|
||||
private static ServiceProvider ConfigureServices(IApplicationInfoService appInfoService)
|
||||
{
|
||||
// TODO: It's in the Labs feed, but we can use Sergio's AOT-friendly source generator for this: https://github.com/CommunityToolkit/Labs-Windows/discussions/463
|
||||
ServiceCollection services = new();
|
||||
@@ -120,7 +127,7 @@ public partial class App : Application, IDisposable
|
||||
|
||||
AddBuiltInCommands(services);
|
||||
|
||||
AddCoreServices(services);
|
||||
AddCoreServices(services, appInfoService);
|
||||
|
||||
AddUIServices(services, dispatcherQueue);
|
||||
|
||||
@@ -171,6 +178,7 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
|
||||
@@ -196,9 +204,11 @@ public partial class App : Application, IDisposable
|
||||
services.AddIconServices(dispatcherQueue);
|
||||
}
|
||||
|
||||
private static void AddCoreServices(ServiceCollection services)
|
||||
private static void AddCoreServices(ServiceCollection services, IApplicationInfoService appInfoService)
|
||||
{
|
||||
// Core services
|
||||
services.AddSingleton(appInfoService);
|
||||
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
services.AddSingleton<IRunHistoryService, RunHistoryService>();
|
||||
|
||||
@@ -206,6 +216,9 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
|
||||
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
|
||||
|
||||
services.AddSingleton<IFuzzyMatcherProvider, FuzzyMatcherProvider>(
|
||||
_ => new FuzzyMatcherProvider(new PrecomputedFuzzyMatcherOptions(), new PinyinFuzzyMatcherOptions()));
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
|
||||
@@ -288,7 +288,6 @@ internal sealed partial class BlurImageControl : Control
|
||||
_effectBrush?.Dispose();
|
||||
_effectBrush = effectFactory.CreateBrush();
|
||||
|
||||
// Set initial source
|
||||
if (ImageSource is not null)
|
||||
{
|
||||
_imageBrush ??= _compositor.CreateSurfaceBrush();
|
||||
|
||||
@@ -16,24 +16,38 @@
|
||||
CornerRadius="8"
|
||||
Translation="0,0,8">
|
||||
<Grid>
|
||||
<!-- Clear style: SolidColorBrush with computed alpha (window backdrop) -->
|
||||
<Border
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
|
||||
Visibility="{x:Bind ClearVisibility, Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind EffectiveClearColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
<!-- Acrylic/Mica style: AcrylicBrush with effective opacity (window backdrop) -->
|
||||
<Border
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Visibility="{x:Bind AcrylicVisibility, Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<AcrylicBrush
|
||||
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
|
||||
TintOpacity="{x:Bind PreviewEffectiveOpacity, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
<!-- Background image (inside window, on top of backdrop) -->
|
||||
<local:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind PreviewBackgroundImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
|
||||
@@ -12,13 +12,11 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class CommandPalettePreview : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color), OnBackdropPropertyChanged));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
||||
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, OnBackgroundImageSourceChanged));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
|
||||
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(1.0));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
|
||||
|
||||
@@ -30,7 +28,18 @@ public sealed partial class CommandPalettePreview : UserControl
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
|
||||
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed, OnVisibilityPropertyChanged));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackdropStyleProperty = DependencyProperty.Register(nameof(PreviewBackdropStyle), typeof(BackdropStyle?), typeof(CommandPalettePreview), new PropertyMetadata(null, OnVisibilityPropertyChanged));
|
||||
|
||||
public static readonly DependencyProperty PreviewEffectiveOpacityProperty = DependencyProperty.Register(nameof(PreviewEffectiveOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(1.0, OnBackdropPropertyChanged));
|
||||
|
||||
// Computed read-only dependency properties
|
||||
public static readonly DependencyProperty EffectiveClearColorProperty = DependencyProperty.Register(nameof(EffectiveClearColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
||||
|
||||
public static readonly DependencyProperty AcrylicVisibilityProperty = DependencyProperty.Register(nameof(AcrylicVisibility), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Visible));
|
||||
|
||||
public static readonly DependencyProperty ClearVisibilityProperty = DependencyProperty.Register(nameof(ClearVisibility), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
public BackgroundImageFit PreviewBackgroundImageFit
|
||||
{
|
||||
@@ -38,12 +47,6 @@ public sealed partial class CommandPalettePreview : UserControl
|
||||
set { SetValue(PreviewBackgroundImageFitProperty, value); }
|
||||
}
|
||||
|
||||
public double PreviewBackgroundOpacity
|
||||
{
|
||||
get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
|
||||
set { SetValue(PreviewBackgroundOpacityProperty, value); }
|
||||
}
|
||||
|
||||
public Color PreviewBackgroundColor
|
||||
{
|
||||
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
|
||||
@@ -56,10 +59,10 @@ public sealed partial class CommandPalettePreview : UserControl
|
||||
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
|
||||
}
|
||||
|
||||
public int PreviewBackgroundImageOpacity
|
||||
public double PreviewBackgroundImageOpacity
|
||||
{
|
||||
get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
|
||||
set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
|
||||
get => (double)GetValue(PreviewBackgroundImageOpacityProperty);
|
||||
set => SetValue(PreviewBackgroundImageOpacityProperty, value);
|
||||
}
|
||||
|
||||
public double PreviewBackgroundImageBrightness
|
||||
@@ -92,12 +95,48 @@ public sealed partial class CommandPalettePreview : UserControl
|
||||
set => SetValue(ShowBackgroundImageProperty, value);
|
||||
}
|
||||
|
||||
public BackdropStyle? PreviewBackdropStyle
|
||||
{
|
||||
get => (BackdropStyle?)GetValue(PreviewBackdropStyleProperty);
|
||||
set => SetValue(PreviewBackdropStyleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the effective opacity for the backdrop, pre-computed by the theme provider.
|
||||
/// For Acrylic style: used directly as TintOpacity.
|
||||
/// For Clear style: used to compute the alpha channel of the solid color.
|
||||
/// </summary>
|
||||
public double PreviewEffectiveOpacity
|
||||
{
|
||||
get => (double)GetValue(PreviewEffectiveOpacityProperty);
|
||||
set => SetValue(PreviewEffectiveOpacityProperty, value);
|
||||
}
|
||||
|
||||
// Computed read-only properties
|
||||
public Color EffectiveClearColor
|
||||
{
|
||||
get => (Color)GetValue(EffectiveClearColorProperty);
|
||||
private set => SetValue(EffectiveClearColorProperty, value);
|
||||
}
|
||||
|
||||
public Visibility AcrylicVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(AcrylicVisibilityProperty);
|
||||
private set => SetValue(AcrylicVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public Visibility ClearVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(ClearVisibilityProperty);
|
||||
private set => SetValue(ClearVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public CommandPalettePreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
private static void OnBackgroundImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not CommandPalettePreview preview)
|
||||
{
|
||||
@@ -107,7 +146,46 @@ public sealed partial class CommandPalettePreview : UserControl
|
||||
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private double ToOpacity(int value) => value / 100.0;
|
||||
private static void OnBackdropPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not CommandPalettePreview preview)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
preview.UpdateComputedClearColor();
|
||||
}
|
||||
|
||||
private static void OnVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not CommandPalettePreview preview)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
preview.UpdateComputedVisibilityProperties();
|
||||
preview.UpdateComputedClearColor();
|
||||
}
|
||||
|
||||
private void UpdateComputedClearColor()
|
||||
{
|
||||
EffectiveClearColor = Color.FromArgb(
|
||||
(byte)(PreviewEffectiveOpacity * 255),
|
||||
PreviewBackgroundColor.R,
|
||||
PreviewBackgroundColor.G,
|
||||
PreviewBackgroundColor.B);
|
||||
}
|
||||
|
||||
private void UpdateComputedVisibilityProperties()
|
||||
{
|
||||
var config = BackdropStyles.Get(PreviewBackdropStyle ?? BackdropStyle.Acrylic);
|
||||
|
||||
// Show backdrop effect based on style (on top of any background image)
|
||||
AcrylicVisibility = config.PreviewBrush == PreviewBrushKind.Acrylic
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
ClearVisibility = config.PreviewBrush == PreviewBrushKind.Solid
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private double ToTintIntensity(int value) => value / 100.0;
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -21,21 +23,19 @@ public sealed partial class ContextMenu : UserControl,
|
||||
IRecipient<UpdateCommandBarMessage>,
|
||||
IRecipient<TryCommandKeybindingMessage>
|
||||
{
|
||||
public ContextMenuViewModel ViewModel { get; } = new();
|
||||
public ContextMenuViewModel ViewModel { get; }
|
||||
|
||||
public ContextMenu()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService<IFuzzyMatcherProvider>());
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||
|
||||
if (ViewModel is not null)
|
||||
{
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(OpenContextMenuMessage message)
|
||||
|
||||
@@ -206,9 +206,10 @@
|
||||
|
||||
<!-- More section -->
|
||||
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="More" />
|
||||
<Border>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Command="{x:Bind ViewModel.OpenInternalToolsCommand}" Content="Open internal tools" />
|
||||
</Border>
|
||||
<Button Command="{x:Bind ViewModel.ToggleDevRibbonVisibilityCommand}" Content="Hide me" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@@ -16,12 +16,12 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
/// </summary>
|
||||
public partial class IconBox : ContentControl
|
||||
{
|
||||
private const double DefaultIconFontSize = 16.0;
|
||||
|
||||
private double _lastScale;
|
||||
private ElementTheme _lastTheme;
|
||||
private double _lastFontSize;
|
||||
|
||||
private const double DefaultIconFontSize = 16.0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
|
||||
/// </summary>
|
||||
@@ -62,6 +62,12 @@ public partial class IconBox : ContentControl
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
#if DEBUG
|
||||
if (_sourceRequested?.GetInvocationList().Length > 1)
|
||||
{
|
||||
Logger.LogWarning("There shouldn't be more than one handler for IconBox.SourceRequested");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
remove => _sourceRequested -= value;
|
||||
}
|
||||
@@ -102,9 +108,12 @@ public partial class IconBox : ContentControl
|
||||
if (Source is FontIconSource fontIcon)
|
||||
{
|
||||
fontIcon.FontSize = _lastFontSize;
|
||||
UpdatePaddingForFontIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePaddingForFontIcon() => Padding = new Thickness(Math.Round(_lastFontSize * -0.2));
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
if (_lastTheme == ActualTheme)
|
||||
@@ -150,10 +159,7 @@ public partial class IconBox : ContentControl
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (SourceKey is not null)
|
||||
{
|
||||
UpdateSourceKey(this, SourceKey);
|
||||
}
|
||||
UpdateSourceKey(this, SourceKey);
|
||||
}
|
||||
|
||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
@@ -170,8 +176,10 @@ public partial class IconBox : ContentControl
|
||||
self.Padding = default;
|
||||
break;
|
||||
case FontIconSource fontIcon:
|
||||
self.UpdateLastFontSize();
|
||||
if (self.Content is IconSourceElement iconSourceElement)
|
||||
{
|
||||
fontIcon.FontSize = self._lastFontSize;
|
||||
iconSourceElement.IconSource = fontIcon;
|
||||
}
|
||||
else
|
||||
@@ -190,7 +198,7 @@ public partial class IconBox : ContentControl
|
||||
self.Content = elem;
|
||||
}
|
||||
|
||||
self.Padding = new Thickness(Math.Round(self._lastFontSize * -0.2));
|
||||
self.UpdatePaddingForFontIcon();
|
||||
|
||||
break;
|
||||
case BitmapIconSource bitmapIcon:
|
||||
@@ -206,10 +214,12 @@ public partial class IconBox : ContentControl
|
||||
self.Padding = default;
|
||||
|
||||
break;
|
||||
|
||||
case IconSource source:
|
||||
self.Content = source.CreateIconElement();
|
||||
self.Padding = default;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
|
||||
}
|
||||
@@ -233,10 +243,10 @@ public partial class IconBox : ContentControl
|
||||
return;
|
||||
}
|
||||
|
||||
Callback(iconBox, sourceKey);
|
||||
RequestIconFromSource(iconBox, sourceKey);
|
||||
}
|
||||
|
||||
private static async void Callback(IconBox iconBox, object? sourceKey)
|
||||
private static async void RequestIconFromSource(IconBox iconBox, object? sourceKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -256,17 +266,12 @@ public partial class IconBox : ContentControl
|
||||
// list virtualization situation, it's very possible we
|
||||
// may have already been set to a new icon before we
|
||||
// even got back from the await.
|
||||
if (eventArgs.Key != sourceKey)
|
||||
if (!ReferenceEquals(sourceKey, iconBox.SourceKey))
|
||||
{
|
||||
// If the requested icon has changed, then just bail
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventArgs.Value == iconBox.Source)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
iconBox.Source = eventArgs.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -24,11 +24,19 @@ internal sealed partial class GridItemContainerStyleSelector : StyleSelector
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
if (item is ListItemViewModel { IsSectionOrSeparator: true } listItem)
|
||||
if (item is not ListItemViewModel element)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(listItem.Title)
|
||||
? Separator!
|
||||
: Section;
|
||||
return Medium;
|
||||
}
|
||||
|
||||
switch (element.Type)
|
||||
{
|
||||
case ListItemType.Separator:
|
||||
return Separator;
|
||||
case ListItemType.SectionHeader:
|
||||
return Section;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -24,15 +25,19 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
if (item is ListItemViewModel element && element.IsSectionOrSeparator)
|
||||
if (item is not ListItemViewModel element)
|
||||
{
|
||||
if (dependencyObject is UIElement li)
|
||||
{
|
||||
li.IsTabStop = false;
|
||||
li.IsHitTestVisible = false;
|
||||
}
|
||||
return Medium;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
switch (element.Type)
|
||||
{
|
||||
case ListItemType.Separator:
|
||||
return Separator;
|
||||
case ListItemType.SectionHeader:
|
||||
return Section;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -18,11 +19,20 @@ internal sealed partial class ListItemContainerStyleSelector : StyleSelector
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
return item switch
|
||||
if (item is not ListItemViewModel element)
|
||||
{
|
||||
ListItemViewModel { IsSectionOrSeparator: true } listItemViewModel when string.IsNullOrWhiteSpace(listItemViewModel.Title) => Separator!,
|
||||
ListItemViewModel { IsSectionOrSeparator: true } => Section,
|
||||
_ => Default,
|
||||
};
|
||||
return Default;
|
||||
}
|
||||
|
||||
switch (element.Type)
|
||||
{
|
||||
case ListItemType.Separator:
|
||||
return Separator;
|
||||
case ListItemType.SectionHeader:
|
||||
return Section;
|
||||
case ListItemType.Item:
|
||||
default:
|
||||
return Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,30 +18,20 @@ public sealed partial class ListItemTemplateSelector : DataTemplateSelector
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
DataTemplate? dataTemplate = ListItem;
|
||||
|
||||
if (container is ListViewItem listItem)
|
||||
if (item is not ListItemViewModel element)
|
||||
{
|
||||
if (item is ListItemViewModel element)
|
||||
{
|
||||
if (container is ListViewItem li && element.IsSectionOrSeparator)
|
||||
{
|
||||
li.IsEnabled = false;
|
||||
li.AllowFocusWhenDisabled = false;
|
||||
li.AllowFocusOnInteraction = false;
|
||||
li.IsHitTestVisible = false;
|
||||
dataTemplate = string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
else
|
||||
{
|
||||
listItem.IsEnabled = true;
|
||||
listItem.AllowFocusWhenDisabled = true;
|
||||
listItem.AllowFocusOnInteraction = true;
|
||||
listItem.IsHitTestVisible = true;
|
||||
}
|
||||
}
|
||||
return ListItem;
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
switch (element.Type)
|
||||
{
|
||||
case ListItemType.Separator:
|
||||
return Separator;
|
||||
case ListItemType.SectionHeader:
|
||||
return Section;
|
||||
case ListItemType.Item:
|
||||
default:
|
||||
return ListItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,23 +206,30 @@
|
||||
x:Key="ListSectionContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="IsEnabled" Value="False" />
|
||||
<Setter Property="AllowFocusWhenDisabled" Value="False" />
|
||||
<Setter Property="AllowFocusOnInteraction" Value="False" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
<Setter Property="Padding" Value="16,8,12,0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Bottom" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSectionMinHeight}" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListSeparatorContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="IsEnabled" Value="False" />
|
||||
<Setter Property="AllowFocusWhenDisabled" Value="False" />
|
||||
<Setter Property="AllowFocusOnInteraction" Value="False" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
<Setter Property="Padding" Value="16,4,12,4" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
|
||||
@@ -1026,10 +1026,7 @@ public sealed partial class ListPage : Page,
|
||||
ItemView.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.IsSeparator(object)"/>
|
||||
/// </summary>
|
||||
private bool IsSeparator(object? item) => item is ListItemViewModel li && li.IsSectionOrSeparator;
|
||||
private bool IsSeparator(object? item) => item is ListItemViewModel li && !li.IsInteractive;
|
||||
|
||||
private enum InputSource
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
@@ -17,19 +18,20 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
||||
/// </summary>
|
||||
internal sealed partial class GlobalErrorHandler : IDisposable
|
||||
{
|
||||
private readonly ErrorReportBuilder _errorReportBuilder = new();
|
||||
private ErrorReportBuilder? _errorReportBuilder;
|
||||
private Options? _options;
|
||||
private App? _app;
|
||||
|
||||
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
|
||||
internal void Register(App app, Options options)
|
||||
internal void Register(App app, Options options, IApplicationInfoService? appInfoService = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
_options = options;
|
||||
|
||||
_app = app;
|
||||
_errorReportBuilder = new ErrorReportBuilder(appInfoService);
|
||||
|
||||
_app.UnhandledException += App_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
@@ -68,7 +70,7 @@ internal sealed partial class GlobalErrorHandler : IDisposable
|
||||
|
||||
if (context == Context.MainThreadException)
|
||||
{
|
||||
var report = _errorReportBuilder.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);
|
||||
var report = _errorReportBuilder!.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);
|
||||
|
||||
StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true);
|
||||
|
||||
|
||||
@@ -219,6 +219,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
iconSize = DefaultIconSize;
|
||||
}
|
||||
|
||||
return IconPathConverter.IconSourceMUX(iconString, false, fontFamily, iconSize);
|
||||
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind ViewModel.BackgroundImageOpacity, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind ViewModel.EffectiveImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
@@ -31,6 +31,6 @@
|
||||
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<pages:ShellPage />
|
||||
<pages:ShellPage HostWindow="{x:Bind}" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -31,6 +31,7 @@ using Windows.ApplicationModel.Activation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
@@ -53,7 +54,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<ErrorOccurredMessage>,
|
||||
IRecipient<DragStartedMessage>,
|
||||
IRecipient<DragCompletedMessage>,
|
||||
IDisposable
|
||||
IRecipient<ToggleDevRibbonMessage>,
|
||||
IDisposable,
|
||||
IHostWindow
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||
@@ -80,15 +83,21 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private int _sessionErrorCount;
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private MicaController? _micaController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private bool _isUpdatingBackdrop;
|
||||
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
private bool _preventHideWhenDeactivated;
|
||||
|
||||
private DevRibbon? _devRibbon;
|
||||
|
||||
private MainWindowViewModel ViewModel { get; }
|
||||
|
||||
public bool IsVisibleToUser { get; private set; } = true;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -109,7 +118,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
||||
}
|
||||
|
||||
SetAcrylic();
|
||||
InitializeBackdropSupport();
|
||||
|
||||
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
|
||||
|
||||
@@ -133,6 +142,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ToggleDevRibbonMessage>(this);
|
||||
|
||||
// Hide our titlebar.
|
||||
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
|
||||
@@ -158,7 +168,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
||||
|
||||
// Make sure that we update the acrylic theme when the OS theme changes
|
||||
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
|
||||
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
|
||||
|
||||
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
|
||||
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
|
||||
@@ -185,7 +195,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
UpdateAcrylic();
|
||||
UpdateBackdrop();
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
@@ -206,7 +216,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Add dev ribbon if enabled
|
||||
if (!BuildInfo.IsCiBuild)
|
||||
{
|
||||
RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
|
||||
_devRibbon = new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) };
|
||||
RootElement.Children.Add(_devRibbon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,48 +291,170 @@ public sealed partial class MainWindow : WindowEx,
|
||||
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
||||
}
|
||||
|
||||
private void SetAcrylic()
|
||||
private void InitializeBackdropSupport()
|
||||
{
|
||||
if (DesktopAcrylicController.IsSupported())
|
||||
if (DesktopAcrylicController.IsSupported() || MicaController.IsSupported())
|
||||
{
|
||||
// Hooking up the policy object.
|
||||
_configurationSource = new SystemBackdropConfiguration
|
||||
{
|
||||
// Initial configuration state.
|
||||
IsInputActive = true,
|
||||
};
|
||||
UpdateAcrylic();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAcrylic()
|
||||
private void UpdateBackdrop()
|
||||
{
|
||||
// Prevent re-entrance when backdrop changes trigger ActualThemeChanged
|
||||
if (_isUpdatingBackdrop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingBackdrop = true;
|
||||
|
||||
var backdrop = _themeService.Current.BackdropParameters;
|
||||
var isImageMode = ViewModel.ShowBackgroundImage;
|
||||
var config = BackdropStyles.Get(backdrop.Style);
|
||||
|
||||
try
|
||||
{
|
||||
if (_acrylicController != null)
|
||||
switch (config.ControllerKind)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
case BackdropControllerKind.Solid:
|
||||
CleanupBackdropControllers();
|
||||
var tintColor = Color.FromArgb(
|
||||
(byte)(backdrop.EffectiveOpacity * 255),
|
||||
backdrop.TintColor.R,
|
||||
backdrop.TintColor.G,
|
||||
backdrop.TintColor.B);
|
||||
SetupTransparentBackdrop(tintColor);
|
||||
break;
|
||||
|
||||
case BackdropControllerKind.Mica:
|
||||
case BackdropControllerKind.MicaAlt:
|
||||
SetupMica(backdrop, isImageMode, config.ControllerKind);
|
||||
break;
|
||||
|
||||
case BackdropControllerKind.Acrylic:
|
||||
case BackdropControllerKind.AcrylicThin:
|
||||
default:
|
||||
SetupDesktopAcrylic(backdrop, isImageMode, config.ControllerKind);
|
||||
break;
|
||||
}
|
||||
|
||||
var backdrop = _themeService.Current.BackdropParameters;
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = backdrop.TintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.LuminosityOpacity,
|
||||
};
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to update backdrop", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingBackdrop = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTransparentBackdrop(Color tintColor)
|
||||
{
|
||||
if (SystemBackdrop is TransparentTintBackdrop existingBackdrop)
|
||||
{
|
||||
existingBackdrop.TintColor = tintColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
SystemBackdrop = new TransparentTintBackdrop { TintColor = tintColor };
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupBackdropControllers()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null;
|
||||
}
|
||||
|
||||
if (_micaController is not null)
|
||||
{
|
||||
_micaController.RemoveAllSystemBackdropTargets();
|
||||
_micaController.Dispose();
|
||||
_micaController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupDesktopAcrylic(BackdropParameters backdrop, bool isImageMode, BackdropControllerKind kind)
|
||||
{
|
||||
CleanupBackdropControllers();
|
||||
|
||||
// Fall back to solid color if acrylic not supported
|
||||
if (_configurationSource is null || !DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
SetupTransparentBackdrop(backdrop.FallbackColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// DesktopAcrylicController and SystemBackdrop can't be active simultaneously
|
||||
SystemBackdrop = null;
|
||||
|
||||
// Image mode: no tint here, BlurImageControl handles it (avoids double-tinting)
|
||||
var effectiveTintOpacity = isImageMode
|
||||
? 0.0f
|
||||
: backdrop.EffectiveOpacity;
|
||||
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
Kind = kind == BackdropControllerKind.AcrylicThin
|
||||
? DesktopAcrylicKind.Thin
|
||||
: DesktopAcrylicKind.Default,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = effectiveTintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
|
||||
};
|
||||
|
||||
// Requires "using WinRT;" for Window.As<>()
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void SetupMica(BackdropParameters backdrop, bool isImageMode, BackdropControllerKind kind)
|
||||
{
|
||||
CleanupBackdropControllers();
|
||||
|
||||
// Fall back to solid color if Mica not supported
|
||||
if (_configurationSource is null || !MicaController.IsSupported())
|
||||
{
|
||||
SetupTransparentBackdrop(backdrop.FallbackColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// MicaController and SystemBackdrop can't be active simultaneously
|
||||
SystemBackdrop = null;
|
||||
_configurationSource.Theme = _themeService.Current.Theme == ElementTheme.Dark
|
||||
? SystemBackdropTheme.Dark
|
||||
: SystemBackdropTheme.Light;
|
||||
|
||||
var hasColorization = _themeService.Current.HasColorization || isImageMode;
|
||||
|
||||
_micaController = new MicaController
|
||||
{
|
||||
Kind = kind == BackdropControllerKind.MicaAlt
|
||||
? MicaKind.BaseAlt
|
||||
: MicaKind.Base,
|
||||
};
|
||||
|
||||
// Only set tint properties when colorization is active
|
||||
// Otherwise let system handle light/dark theme defaults automatically
|
||||
if (hasColorization)
|
||||
{
|
||||
// Image mode: no tint here, BlurImageControl handles it (avoids double-tinting)
|
||||
_micaController.TintColor = backdrop.TintColor;
|
||||
_micaController.TintOpacity = isImageMode ? 0.0f : backdrop.EffectiveOpacity;
|
||||
_micaController.FallbackColor = backdrop.FallbackColor;
|
||||
_micaController.LuminosityOpacity = backdrop.EffectiveLuminosityOpacity;
|
||||
}
|
||||
|
||||
_micaController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_micaController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
@@ -575,17 +708,14 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
IsVisibleToUser = false;
|
||||
}
|
||||
|
||||
wasCloaked = hr.Succeeded;
|
||||
}
|
||||
|
||||
if (wasCloaked)
|
||||
{
|
||||
// Because we're only cloaking the window, bury it at the bottom in case something can
|
||||
// see it - e.g. some accessibility helper (note: this also removes the top-most status).
|
||||
PInvoke.SetWindowPos(_hwnd, HWND.HWND_BOTTOM, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
}
|
||||
|
||||
return wasCloaked;
|
||||
}
|
||||
|
||||
@@ -595,6 +725,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
BOOL value = false;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
||||
IsVisibleToUser = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,12 +768,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null!;
|
||||
_configurationSource = null!;
|
||||
}
|
||||
CleanupBackdropControllers();
|
||||
_configurationSource = null!;
|
||||
}
|
||||
|
||||
// Updates our window s.t. the top of the window is draggable.
|
||||
@@ -1012,6 +1139,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
DisposeAcrylic();
|
||||
}
|
||||
|
||||
public void Receive(ToggleDevRibbonMessage message)
|
||||
{
|
||||
_devRibbon?.Visibility = _devRibbon.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public void Receive(DragStartedMessage message)
|
||||
{
|
||||
_preventHideWhenDeactivated = true;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record ToggleDevRibbonMessage;
|
||||
@@ -141,6 +141,7 @@
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
|
||||
|
||||
@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.Settings;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -72,6 +73,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public IHostWindow? HostWindow { get; set; }
|
||||
|
||||
public ShellPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
@@ -435,7 +438,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
if (!RootFrame.CanGoBack)
|
||||
{
|
||||
ViewModel.GoHome();
|
||||
ViewModel.GoHome(withAnimation, focusSearch);
|
||||
}
|
||||
|
||||
if (focusSearch)
|
||||
@@ -531,6 +534,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
if (shouldSearchBoxBeVisible || page is not ContentPage)
|
||||
{
|
||||
if (HostWindow?.IsVisibleToUser != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.IsSearchBoxVisible = shouldSearchBoxBeVisible;
|
||||
SearchBox.Focus(FocusState.Programmatic);
|
||||
SearchBox.SelectSearch();
|
||||
@@ -547,6 +555,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
try
|
||||
{
|
||||
if (HostWindow?.IsVisibleToUser != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await page.DispatcherQueue.EnqueueAsync(
|
||||
async () =>
|
||||
{
|
||||
@@ -556,6 +569,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (HostWindow?.IsVisibleToUser != true)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
||||
{
|
||||
var set = frameworkElement.Focus(FocusState.Programmatic);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
@@ -23,13 +24,13 @@ internal sealed class PowerToysRootPageService : IRootPageService
|
||||
private IExtensionWrapper? _activeExtension;
|
||||
private Lazy<MainListPage> _mainListPage;
|
||||
|
||||
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
||||
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel, IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
{
|
||||
_tlcManager = topLevelCommandManager;
|
||||
|
||||
_mainListPage = new Lazy<MainListPage>(() =>
|
||||
{
|
||||
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel);
|
||||
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel, fuzzyMatcherProvider);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
@@ -65,6 +66,25 @@ internal sealed class Program
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Starting at {DateTime.UtcNow}");
|
||||
|
||||
// Log application startup information
|
||||
try
|
||||
{
|
||||
var appInfoService = new ApplicationInfoService(() => Logger.CurrentVersionLogDirectoryPath);
|
||||
var startupMessage = $"""
|
||||
============================================================
|
||||
Hello World! Command Palette is starting.
|
||||
|
||||
{appInfoService.GetApplicationInfoSummary()}
|
||||
============================================================
|
||||
""";
|
||||
Logger.LogInfo(startupMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to log application startup information", ex);
|
||||
}
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted());
|
||||
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.UI;
|
||||
@@ -34,7 +35,7 @@ internal sealed class ColorfulThemeProvider : IThemeProvider
|
||||
_uiSettings = uiSettings;
|
||||
}
|
||||
|
||||
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
|
||||
public BackdropParameters GetBackdropParameters(ThemeContext context)
|
||||
{
|
||||
var isLight = context.Theme == ElementTheme.Light ||
|
||||
(context.Theme == ElementTheme.Default &&
|
||||
@@ -53,7 +54,26 @@ internal sealed class ColorfulThemeProvider : IThemeProvider
|
||||
var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser;
|
||||
var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity);
|
||||
|
||||
return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f);
|
||||
var transparencyMode = context.BackdropStyle ?? BackdropStyle.Acrylic;
|
||||
var config = BackdropStyles.Get(transparencyMode);
|
||||
|
||||
// For colorful theme, boost tint opacity to show color better through blur
|
||||
// But not for styles with fixed opacity (Mica) - they handle their own opacity
|
||||
var baseTintOpacity = config.ControllerKind == BackdropControllerKind.Solid || !config.SupportsOpacity
|
||||
? (float?)null // Use default
|
||||
: Math.Max(config.BaseTintOpacity, 0.8f);
|
||||
|
||||
var effectiveOpacity = config.ComputeEffectiveOpacity(context.BackdropOpacity, baseTintOpacity);
|
||||
var effectiveLuminosityOpacity = config.SupportsOpacity
|
||||
? config.BaseLuminosityOpacity * context.BackdropOpacity
|
||||
: config.BaseLuminosityOpacity;
|
||||
|
||||
return new BackdropParameters(
|
||||
TintColor: effectiveBgColor,
|
||||
FallbackColor: effectiveBgColor,
|
||||
EffectiveOpacity: effectiveOpacity,
|
||||
EffectiveLuminosityOpacity: effectiveLuminosityOpacity,
|
||||
Style: transparencyMode);
|
||||
}
|
||||
|
||||
private static class ColorBlender
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents abstract host window functionality.
|
||||
/// </summary>
|
||||
public interface IHostWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the window is visible to the user, taking account not only window visibility but also cloaking.
|
||||
/// </summary>
|
||||
bool IsVisibleToUser { get; }
|
||||
}
|
||||
@@ -8,14 +8,14 @@ using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides theme identification, resource path resolution, and creation of acrylic
|
||||
/// backdrop parameters based on the current <see cref="ThemeContext"/>.
|
||||
/// Provides theme identification, resource path resolution, and creation of backdrop
|
||||
/// parameters based on the current <see cref="ThemeContext"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations should expose a stable <see cref="ThemeKey"/> and a valid XAML resource
|
||||
/// dictionary path via <see cref="ResourcePath"/>. The
|
||||
/// <see cref="GetAcrylicBackdrop(ThemeContext)"/> method computes
|
||||
/// <see cref="AcrylicBackdropParameters"/> using the supplied theme context.
|
||||
/// <see cref="GetBackdropParameters(ThemeContext)"/> method computes
|
||||
/// <see cref="BackdropParameters"/> using the supplied theme context.
|
||||
/// </remarks>
|
||||
internal interface IThemeProvider
|
||||
{
|
||||
@@ -30,9 +30,9 @@ internal interface IThemeProvider
|
||||
string ResourcePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates acrylic backdrop parameters based on the provided theme context.
|
||||
/// Creates backdrop parameters based on the provided theme context.
|
||||
/// </summary>
|
||||
/// <param name="context">The current theme context, including theme, tint, and optional background details.</param>
|
||||
/// <returns>The computed <see cref="AcrylicBackdropParameters"/> for the backdrop.</returns>
|
||||
AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context);
|
||||
/// <param name="context">The current theme context, including theme, tint, transparency mode, and optional background details.</param>
|
||||
/// <returns>The computed <see cref="BackdropParameters"/> for the backdrop.</returns>
|
||||
BackdropParameters GetBackdropParameters(ThemeContext context);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.UI;
|
||||
@@ -28,16 +29,28 @@ internal sealed class NormalThemeProvider : IThemeProvider
|
||||
|
||||
public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml";
|
||||
|
||||
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
|
||||
public BackdropParameters GetBackdropParameters(ThemeContext context)
|
||||
{
|
||||
var isLight = context.Theme == ElementTheme.Light ||
|
||||
(context.Theme == ElementTheme.Default &&
|
||||
_uiSettings.GetColorValue(UIColorType.Background).R > 128);
|
||||
|
||||
return new AcrylicBackdropParameters(
|
||||
var backdropStyle = context.BackdropStyle ?? BackdropStyle.Acrylic;
|
||||
var config = BackdropStyles.Get(backdropStyle);
|
||||
|
||||
// Apply light/dark theme adjustment to luminosity
|
||||
var baseLuminosityOpacity = isLight
|
||||
? config.BaseLuminosityOpacity
|
||||
: Math.Min(config.BaseLuminosityOpacity + 0.06f, 1.0f);
|
||||
|
||||
var effectiveOpacity = config.ComputeEffectiveOpacity(context.BackdropOpacity);
|
||||
var effectiveLuminosityOpacity = baseLuminosityOpacity * context.BackdropOpacity;
|
||||
|
||||
return new BackdropParameters(
|
||||
TintColor: isLight ? LightBaseColor : DarkBaseColor,
|
||||
FallbackColor: isLight ? LightBaseColor : DarkBaseColor,
|
||||
TintOpacity: 0.5f,
|
||||
LuminosityOpacity: isLight ? 0.9f : 0.96f);
|
||||
EffectiveOpacity: effectiveOpacity,
|
||||
EffectiveLuminosityOpacity: effectiveLuminosityOpacity,
|
||||
Style: backdropStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Input parameters for theme computation, passed to theme providers.
|
||||
/// </summary>
|
||||
internal sealed record ThemeContext
|
||||
{
|
||||
public ElementTheme Theme { get; init; }
|
||||
@@ -21,4 +25,8 @@ internal sealed record ThemeContext
|
||||
public double BackgroundImageOpacity { get; init; }
|
||||
|
||||
public int? ColorIntensity { get; init; }
|
||||
|
||||
public BackdropStyle? BackdropStyle { get; init; }
|
||||
|
||||
public float BackdropOpacity { get; init; } = 1.0f;
|
||||
}
|
||||
|
||||
@@ -72,10 +72,13 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
}
|
||||
|
||||
// provider selection
|
||||
var intensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
|
||||
IThemeProvider provider = intensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
|
||||
? _colorfulThemeProvider
|
||||
: _normalThemeProvider;
|
||||
var themeColorIntensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
|
||||
var imageTintIntensity = Math.Clamp(_settings.BackgroundImageTintIntensity, 0, 100);
|
||||
var effectiveColorIntensity = _settings.ColorizationMode == ColorizationMode.Image
|
||||
? imageTintIntensity
|
||||
: themeColorIntensity;
|
||||
|
||||
IThemeProvider provider = UseColorfulProvider(effectiveColorIntensity) ? _colorfulThemeProvider : _normalThemeProvider;
|
||||
|
||||
// Calculate values
|
||||
var tint = _settings.ColorizationMode switch
|
||||
@@ -96,32 +99,39 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
};
|
||||
var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
|
||||
|
||||
// create context and offload to actual theme provider
|
||||
// create input and offload to actual theme provider
|
||||
var context = new ThemeContext
|
||||
{
|
||||
Tint = tint,
|
||||
ColorIntensity = intensity,
|
||||
ColorIntensity = effectiveColorIntensity,
|
||||
Theme = effectiveTheme,
|
||||
BackgroundImageSource = imageSource,
|
||||
BackgroundImageStretch = stretch,
|
||||
BackgroundImageOpacity = opacity,
|
||||
BackdropStyle = _settings.BackdropStyle,
|
||||
BackdropOpacity = Math.Clamp(_settings.BackdropOpacity, 0, 100) / 100f,
|
||||
};
|
||||
var backdrop = provider.GetAcrylicBackdrop(context);
|
||||
var backdrop = provider.GetBackdropParameters(context);
|
||||
var blur = _settings.BackgroundImageBlurAmount;
|
||||
var brightness = _settings.BackgroundImageBrightness;
|
||||
|
||||
// Create public snapshot (no provider!)
|
||||
var hasColorization = effectiveColorIntensity > 0
|
||||
&& _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
var snapshot = new ThemeSnapshot
|
||||
{
|
||||
Tint = tint,
|
||||
TintIntensity = intensity / 100f,
|
||||
TintIntensity = effectiveColorIntensity / 100f,
|
||||
Theme = effectiveTheme,
|
||||
BackgroundImageSource = imageSource,
|
||||
BackgroundImageStretch = stretch,
|
||||
BackgroundImageOpacity = opacity,
|
||||
BackdropParameters = backdrop,
|
||||
BackdropOpacity = context.BackdropOpacity,
|
||||
BlurAmount = blur,
|
||||
BackgroundBrightness = brightness / 100f,
|
||||
HasColorization = hasColorization,
|
||||
};
|
||||
|
||||
// Bundle with provider for internal use
|
||||
@@ -138,6 +148,12 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
|
||||
}
|
||||
|
||||
private bool UseColorfulProvider(int effectiveColorIntensity)
|
||||
{
|
||||
return _settings.ColorizationMode == ColorizationMode.Image
|
||||
|| (effectiveColorIntensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor);
|
||||
}
|
||||
|
||||
private static BitmapImage? LoadImageSafe(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
@@ -195,13 +211,15 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
{
|
||||
Tint = Colors.Transparent,
|
||||
Theme = ElementTheme.Light,
|
||||
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
|
||||
BackdropParameters = new BackdropParameters(Colors.Black, Colors.Black, EffectiveOpacity: 0.5f, EffectiveLuminosityOpacity: 0.5f),
|
||||
BackdropOpacity = 1.0f,
|
||||
BackgroundImageOpacity = 1,
|
||||
BackgroundImageSource = null,
|
||||
BackgroundImageStretch = Stretch.Fill,
|
||||
BlurAmount = 0,
|
||||
TintIntensity = 1.0f,
|
||||
BackgroundBrightness = 0,
|
||||
HasColorization = false,
|
||||
},
|
||||
Provider = _normalThemeProvider,
|
||||
};
|
||||
|
||||
@@ -22,18 +22,50 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<ptControls:ScreenPreview Margin="0,0,0,16" HorizontalAlignment="Left">
|
||||
<ptControls:CommandPalettePreview
|
||||
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
|
||||
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
|
||||
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
|
||||
PreviewBackgroundImageFit="{x:Bind ViewModel.Appearance.BackgroundImageFit, Mode=OneWay}"
|
||||
PreviewBackgroundImageSource="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageSource, Mode=OneWay}"
|
||||
PreviewBackgroundImageTint="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}"
|
||||
PreviewBackgroundImageTintIntensity="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=OneWay}"
|
||||
PreviewBackgroundOpacity="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintOpacity, Mode=OneWay}"
|
||||
RequestedTheme="{x:Bind ViewModel.Appearance.EffectiveTheme, Mode=OneWay}" />
|
||||
</ptControls:ScreenPreview>
|
||||
<StackPanel
|
||||
Margin="0,0,0,16"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal"
|
||||
Spacing="16">
|
||||
<ptControls:ScreenPreview>
|
||||
<ptControls:CommandPalettePreview
|
||||
PreviewBackdropStyle="{x:Bind ViewModel.Appearance.EffectiveBackdropStyle, Mode=OneWay}"
|
||||
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
|
||||
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
|
||||
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
|
||||
PreviewBackgroundImageFit="{x:Bind ViewModel.Appearance.BackgroundImageFit, Mode=OneWay}"
|
||||
PreviewBackgroundImageOpacity="{x:Bind ViewModel.Appearance.EffectiveImageOpacity, Mode=OneWay}"
|
||||
PreviewBackgroundImageSource="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageSource, Mode=OneWay}"
|
||||
PreviewBackgroundImageTint="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}"
|
||||
PreviewBackgroundImageTintIntensity="{x:Bind ViewModel.Appearance.EffectiveTintIntensity, Mode=OneWay}"
|
||||
PreviewEffectiveOpacity="{x:Bind ViewModel.Appearance.EffectiveBackdrop.EffectiveOpacity, Mode=OneWay}"
|
||||
RequestedTheme="{x:Bind ViewModel.Appearance.EffectiveTheme, Mode=OneWay}" />
|
||||
</ptControls:ScreenPreview>
|
||||
<StackPanel VerticalAlignment="Bottom" Spacing="8">
|
||||
<Button
|
||||
x:Uid="Settings_AppearancePage_OpenCommandPaletteButton"
|
||||
MinWidth="200"
|
||||
HorizontalContentAlignment="Left"
|
||||
Click="OpenCommandPalette_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_AppearancePage_OpenCommandPaletteButton_Text" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button
|
||||
x:Uid="Settings_AppearancePage_ResetAppearanceButton"
|
||||
MinWidth="200"
|
||||
HorizontalContentAlignment="Left"
|
||||
Command="{x:Bind ViewModel.Appearance.ResetAppearanceSettingsCommand}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_AppearancePage_ResetAppearanceButton_Text" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
|
||||
@@ -62,19 +94,67 @@
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="Settings_GeneralPage_BackdropStyle_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.BackdropStyleIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Acrylic" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Transparent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Mica" />
|
||||
<!-- Hidden: preview not working well, kept to preserve index mapping -->
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_AcrylicThin" Visibility="Collapsed" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_MicaAlt" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- Mica description (no opacity control) -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="Settings_GeneralPage_MicaBackdrop_SettingsCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsMicaBackdropDescriptionVisible, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="Settings_GeneralPage_MicaBackdrop_DescriptionTextBlock"
|
||||
Margin="24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</controls:SettingsCard>
|
||||
<!-- Opacity slider (for non-Mica styles) -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackdropOpacity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.BackdropOpacity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="Settings_GeneralPage_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}"
|
||||
IsExpanded="{x:Bind ViewModel.Appearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||
<ComboBox
|
||||
x:Uid="Settings_GeneralPage_ColorizationMode"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
||||
</ComboBox>
|
||||
<Grid>
|
||||
<ComboBox
|
||||
x:Uid="Settings_GeneralPage_ColorizationMode"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
x:Uid="Settings_GeneralPage_Background_NotAvailable"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundNotAvailableVisible, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- none -->
|
||||
<controls:SettingsCard
|
||||
@@ -155,7 +235,7 @@
|
||||
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
|
||||
SelectedColor="{x:Bind ViewModel.Appearance.ThemeColor, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsColorIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
@@ -163,9 +243,17 @@
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ImageTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsImageTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.BackgroundImageTintIntensity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Reset background image properties -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<!-- Reset appearance properties -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsResetButtonVisible, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI;
|
||||
@@ -12,6 +17,7 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.Windows.Storage.Pickers;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
@@ -86,4 +92,9 @@ public sealed partial class AppearancePage : Page
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OpenCommandPalette_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(string.Empty, HWND.Null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
@@ -19,6 +17,7 @@ public sealed partial class GeneralPage : Page
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
private readonly SettingsViewModel? viewModel;
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
|
||||
public GeneralPage()
|
||||
{
|
||||
@@ -27,6 +26,7 @@ public sealed partial class GeneralPage : Page
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
|
||||
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
||||
}
|
||||
|
||||
@@ -35,55 +35,8 @@ public sealed partial class GeneralPage : Page
|
||||
get
|
||||
{
|
||||
var versionNo = ResourceLoaderInstance.GetString("Settings_GeneralPage_VersionNo");
|
||||
if (!TryGetPackagedVersion(out var version) && !TryGetAssemblyVersion(out version))
|
||||
{
|
||||
version = "?";
|
||||
}
|
||||
|
||||
var version = _appInfoService.AppVersion;
|
||||
return string.Format(CultureInfo.CurrentCulture, versionNo, version);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetPackagedVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
// Package.Current throws InvalidOperationException if the app is not packaged
|
||||
var v = Package.Current.Id.Version;
|
||||
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get version from the package", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetAssemblyVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(processPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var info = FileVersionInfo.GetVersionInfo(processPath);
|
||||
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get version from the executable", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard
|
||||
x:Name="ToggleDevRibbonVisibilitySettingsCard"
|
||||
Description="This is only temporary and state is not saved"
|
||||
Header="Toggle dev ribbon visibility"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button Click="ToggleDevRibbonClicked" Content="Toggle dev ribbon" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Data Section -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Data and Files" />
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.System;
|
||||
using Page = Microsoft.UI.Xaml.Controls.Page;
|
||||
@@ -15,9 +19,13 @@ namespace Microsoft.CmdPal.UI.Settings;
|
||||
/// </summary>
|
||||
public sealed partial class InternalPage : Page
|
||||
{
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
|
||||
public InternalPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
|
||||
}
|
||||
|
||||
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
|
||||
@@ -46,7 +54,7 @@ public sealed partial class InternalPage : Page
|
||||
{
|
||||
try
|
||||
{
|
||||
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
|
||||
var logFolderPath = _appInfoService.LogDirectory;
|
||||
if (Directory.Exists(logFolderPath))
|
||||
{
|
||||
await Launcher.LaunchFolderPathAsync(logFolderPath);
|
||||
@@ -78,7 +86,7 @@ public sealed partial class InternalPage : Page
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
var directory = _appInfoService.ConfigDirectory;
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
await Launcher.LaunchFolderPathAsync(directory);
|
||||
@@ -89,4 +97,9 @@ public sealed partial class InternalPage : Page
|
||||
Logger.LogError("Failed to open directory in Explorer", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleDevRibbonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new ToggleDevRibbonMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,6 +577,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color intensity</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ImageTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color intensity</value>
|
||||
</data>
|
||||
<data name="OptionalColorPickerButton_UnsetTextBlock.Text" xml:space="preserve">
|
||||
<value>Choose color</value>
|
||||
</data>
|
||||
@@ -668,7 +671,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Background image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
||||
<value>No settings</value>
|
||||
<value>No additional settings are available.</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_Background_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background</value>
|
||||
@@ -676,6 +679,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_Background_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose a custom background color or image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_Background_NotAvailable.Text" xml:space="preserve">
|
||||
<value>Not available with Mica</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
|
||||
<value>System accent color</value>
|
||||
</data>
|
||||
@@ -692,7 +698,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Restore defaults</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_Background_ResetImagePropertiesButton.Content" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
<value>Reset image settings</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1.Text" xml:space="preserve">
|
||||
<value>Change the system accent in Windows Settings:</value>
|
||||
@@ -727,4 +733,48 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||
<value>Version {0}</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropOpacity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Opacity</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Material</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Select the visual material used for the window background</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_Acrylic.Content" xml:space="preserve">
|
||||
<value>Acrylic (default)</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_Transparent.Content" xml:space="preserve">
|
||||
<value>Transparent</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_Mica.Content" xml:space="preserve">
|
||||
<value>Mica</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_AcrylicThin.Content" xml:space="preserve">
|
||||
<value>Thin Acrylic</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropStyle_MicaAlt.Content" xml:space="preserve">
|
||||
<value>Mica Alt</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_MicaBackdrop_DescriptionTextBlock.Text" xml:space="preserve">
|
||||
<value>Mica automatically adapts to your desktop wallpaper. Custom backgrounds and opacity settings are not available for this material.</value>
|
||||
</data>
|
||||
<data name="Settings_AppearancePage_OpenCommandPaletteButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Button to open the Command Palette window to preview appearance changes</comment>
|
||||
</data>
|
||||
<data name="Settings_AppearancePage_OpenCommandPaletteButton_Text.Text" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
</data>
|
||||
<data name="Settings_AppearancePage_ResetAppearanceButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Reset appearance settings</value>
|
||||
<comment>Button to reset all appearance settings to their default values</comment>
|
||||
</data>
|
||||
<data name="Settings_AppearancePage_ResetAppearanceButton_Text.Text" xml:space="preserve">
|
||||
<value>Reset to defaults</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_More_Button.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>More options</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -107,6 +107,12 @@ internal sealed partial class DevRibbonViewModel : ObservableObject
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Internal"));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleDevRibbonVisibility()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new ToggleDevRibbonMessage());
|
||||
}
|
||||
|
||||
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
|
||||
{
|
||||
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
|
||||
|
||||
@@ -94,40 +94,49 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
|
||||
// Arguments:
|
||||
// - path: the full, expanded path to the icon.
|
||||
// - targetSize: the target size for decoding/rasterizing the icon.
|
||||
// Return Value:
|
||||
// - An IconElement with its IconSource set, if possible.
|
||||
template<typename TIconSource>
|
||||
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome)
|
||||
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, int targetSize)
|
||||
{
|
||||
// FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters.
|
||||
// To skip throwing on Uri construction, we can quickly check if the first character is ASCII.
|
||||
if (!path.empty() && path.front() < 128)
|
||||
if (path.empty() || path.front() >= 128)
|
||||
{
|
||||
try
|
||||
{
|
||||
winrt::Windows::Foundation::Uri iconUri{ path };
|
||||
|
||||
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
iconSource.ImageSource(source);
|
||||
return iconSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
typename BitmapIconSource<TIconSource>::type iconSource;
|
||||
// Make sure to set this to false, so we keep the RGB data of the
|
||||
// image. Otherwise, the icon will be white for all the
|
||||
// non-transparent pixels in the image.
|
||||
iconSource.ShowAsMonochrome(monochrome);
|
||||
iconSource.UriSource(iconUri);
|
||||
return iconSource;
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
winrt::Windows::Foundation::Uri iconUri{ path };
|
||||
|
||||
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
source.RasterizePixelWidth(static_cast<double>(targetSize));
|
||||
// Set only single dimension here; the image might not be square and
|
||||
// this will preserve the aspect ratio (for the price of keeping height unbound).
|
||||
// source.RasterizePixelHeight(static_cast<double>(targetSize));
|
||||
iconSource.ImageSource(source);
|
||||
return iconSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::BitmapImage bitmapImage;
|
||||
bitmapImage.DecodePixelWidth(targetSize);
|
||||
// Set only single dimension here; the image might not be square and
|
||||
// this will preserve the aspect ratio (for the price of keeping height unbound).
|
||||
// bitmapImage.DecodePixelHeight(targetSize);
|
||||
bitmapImage.UriSource(iconUri);
|
||||
iconSource.ImageSource(bitmapImage);
|
||||
return iconSource;
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -158,14 +167,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// Return Value:
|
||||
// - An IconElement with its IconSource set, if possible.
|
||||
template<typename TIconSource>
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
TIconSource iconSource{ nullptr };
|
||||
|
||||
if (iconPath.size() != 0)
|
||||
{
|
||||
const auto expandedIconPath{ _expandIconPath(iconPath) };
|
||||
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, monochrome);
|
||||
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, targetSize);
|
||||
|
||||
// If we fail to set the icon source using the "icon" as a path,
|
||||
// let's try it as a symbol/emoji.
|
||||
@@ -235,9 +244,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
||||
// }
|
||||
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize);
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
||||
@@ -352,7 +361,6 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
}
|
||||
|
||||
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
||||
const bool monochrome,
|
||||
const winrt::hstring& fontFamily,
|
||||
const int targetSize)
|
||||
{
|
||||
@@ -360,7 +368,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize);
|
||||
return _IconSourceMUX(iconPath, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
||||
@@ -374,13 +382,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath) {
|
||||
return IconMUX(iconPath, 24);
|
||||
}
|
||||
|
||||
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath, const int targetSize)
|
||||
{
|
||||
std::wstring_view iconPathWithoutIndex;
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
auto source = IconSourceMUX(iconPath, false, L"", targetSize);
|
||||
auto source = IconSourceMUX(iconPath, L"", targetSize);
|
||||
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
||||
icon.IconSource(source);
|
||||
return icon;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
//static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath);
|
||||
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const winrt::hstring& fontFamily, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
|
||||
{
|
||||
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
||||
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily, Int32 targetSize);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, String fontFamily, Int32 targetSize);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user