mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-14 18:57:55 +01:00
Merge branch 'dev/mjolley/deux' of https://github.com/microsoft/PowerToys into dev/mjolley/deux
This commit is contained in:
4
.github/actions/spell-check/allow/code.txt
vendored
4
.github/actions/spell-check/allow/code.txt
vendored
@@ -335,3 +335,7 @@ azp
|
||||
feedbackhub
|
||||
needinfo
|
||||
reportbug
|
||||
|
||||
#ffmpeg
|
||||
crf
|
||||
nostdin
|
||||
|
||||
12
.github/actions/spell-check/expect.txt
vendored
12
.github/actions/spell-check/expect.txt
vendored
@@ -141,8 +141,11 @@ BITSPIXEL
|
||||
bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
Blockquotes
|
||||
blt
|
||||
bluelightreduction
|
||||
bluelightreductionstate
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
bmi
|
||||
@@ -250,6 +253,7 @@ colorformat
|
||||
colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -1113,6 +1117,7 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
nightlight
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1483,6 +1488,7 @@ rgh
|
||||
rgn
|
||||
rgs
|
||||
rguid
|
||||
rhk
|
||||
RIDEV
|
||||
RIGHTSCROLLBAR
|
||||
riid
|
||||
@@ -1588,6 +1594,7 @@ SHGDNF
|
||||
SHGFI
|
||||
SHIL
|
||||
shinfo
|
||||
shk
|
||||
shlwapi
|
||||
shobjidl
|
||||
SHORTCUTATLEAST
|
||||
@@ -1798,6 +1805,7 @@ tlbimp
|
||||
tlc
|
||||
tmain
|
||||
TNP
|
||||
toolgood
|
||||
Toolhelp
|
||||
toolwindow
|
||||
TOPDOWNDIB
|
||||
@@ -1846,6 +1854,8 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
@@ -1857,8 +1867,10 @@ Uniquifies
|
||||
unitconverter
|
||||
unittests
|
||||
UNLEN
|
||||
Uninitializes
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
"PowerToys.FancyZonesEditorCommon.dll",
|
||||
"PowerToys.FancyZonesModuleInterface.dll",
|
||||
"PowerToys.FancyZones.exe",
|
||||
"FancyZonesCLI.exe",
|
||||
"FancyZonesCLI.dll",
|
||||
|
||||
"PowerToys.GcodePreviewHandler.dll",
|
||||
"PowerToys.GcodePreviewHandler.exe",
|
||||
@@ -351,6 +353,11 @@
|
||||
"Microsoft.SemanticKernel.Connectors.Ollama.dll",
|
||||
"OllamaSharp.dll",
|
||||
|
||||
"boost_regex-vc143-mt-gd-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-gd-x64-1_87.dll",
|
||||
"boost_regex-vc143-mt-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-x64-1_87.dll",
|
||||
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll"
|
||||
|
||||
@@ -27,7 +27,8 @@ $versionExceptions = @(
|
||||
"WyHash.dll",
|
||||
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
|
||||
"ObjectModelCsProjection.dll",
|
||||
"RendererCsProjection.dll") -join '|';
|
||||
"RendererCsProjection.dll",
|
||||
"Microsoft.ML.OnnxRuntime.dll") -join '|';
|
||||
$nullVersionExceptions = @(
|
||||
"SkiaSharp.Views.WinUI.Native.dll",
|
||||
"libSkiaSharp.dll",
|
||||
@@ -52,7 +53,12 @@ $nullVersionExceptions = @(
|
||||
"System.Diagnostics.EventLog.Messages.dll",
|
||||
"Microsoft.Windows.Widgets.dll",
|
||||
"AdaptiveCards.ObjectModel.WinUI3.dll",
|
||||
"AdaptiveCards.Rendering.WinUI3.dll") -join '|';
|
||||
"AdaptiveCards.Rendering.WinUI3.dll",
|
||||
"boost_regex_vc143_mt_gd_x32_1_87.dll",
|
||||
"boost_regex_vc143_mt_gd_x64_1_87.dll",
|
||||
"boost_regex_vc143_mt_x32_1_87.dll",
|
||||
"boost_regex_vc143_mt_x64_1_87.dll"
|
||||
) -join '|';
|
||||
$totalFailure = 0;
|
||||
|
||||
Write-Host $DirPath;
|
||||
|
||||
@@ -121,6 +121,9 @@ PowerToys Awake is a tool to keep your computer awake.
|
||||
|
||||
Randy contributed Registry Preview and some very early conversations about keyboard remapping.
|
||||
|
||||
### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon
|
||||
Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product
|
||||
|
||||
### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
|
||||
|
||||
Find My Mouse is based on Raymond Chen's SuperSonar.
|
||||
@@ -180,7 +183,6 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
|
||||
## PowerToys core team
|
||||
|
||||
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead
|
||||
- [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager
|
||||
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
|
||||
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead
|
||||
@@ -209,6 +211,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
## Former PowerToys core team members
|
||||
|
||||
- [@indierawk2k2](https://github.com/indierawk2k2) - Mike Harsh - Product Manager
|
||||
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager
|
||||
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager
|
||||
- [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager
|
||||
- [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
@@ -118,6 +119,7 @@
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -75,6 +75,37 @@ OTHER DEALINGS IN THE SOFTWARE.
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
```
|
||||
|
||||
### ToolGood.Words.Pinyin
|
||||
|
||||
We use the ToolGood.Words.Pinyin NuGet package for converting Chinese characters to pinyin.
|
||||
|
||||
**Source**: [https://github.com/toolgood/ToolGood.Words.Pinyin](https://github.com/toolgood/ToolGood.Words.Pinyin)
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 ToolGood
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
|
||||
## Utility: Command Palette Built-in Extensions
|
||||
|
||||
### Calculator
|
||||
@@ -1532,6 +1563,7 @@ SOFTWARE.
|
||||
- SkiaSharp.Views.WinUI
|
||||
- StreamJsonRpc
|
||||
- StyleCop.Analyzers
|
||||
- ToolGood.Words.Pinyin
|
||||
- UnicodeInformation
|
||||
- UnitsNet
|
||||
- UTF.Unknown
|
||||
|
||||
@@ -398,6 +398,10 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/fancyzones/FancyZones/FancyZones.vcxproj" Id="ff1d7936-842a-4bbb-8bea-e9fe796de700" />
|
||||
<Project Path="src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/fancyzones/FancyZonesEditorCommon/FancyZonesEditorCommon.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -38,7 +38,7 @@ For C# modules, the settings are accessed through the `SettingsUtils` class in t
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
// Read settings
|
||||
var settings = SettingsUtils.GetSettings<ModuleSettings>("ModuleName");
|
||||
var settings = SettingsUtils.Default.GetSettings<ModuleSettings>("ModuleName");
|
||||
bool enabled = settings.Enabled;
|
||||
```
|
||||
|
||||
@@ -49,7 +49,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
// Write settings
|
||||
settings.Enabled = true;
|
||||
SettingsUtils.SaveSettings(settings.ToJsonString(), "ModuleName");
|
||||
SettingsUtils.Default.SaveSettings(settings.ToJsonString(), "ModuleName");
|
||||
```
|
||||
|
||||
## Settings Handling in Modules
|
||||
|
||||
@@ -16,4 +16,5 @@ public static partial class CLSID
|
||||
public static readonly Guid CollatorDataSource = new Guid("9E175B8B-F52A-11D8-B9A5-505054503030");
|
||||
public static readonly Guid ApplicationActivationManager = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
|
||||
public static readonly Guid VirtualDesktopManager = new("aa509086-5ca9-4c25-8f95-589d3c07b48a");
|
||||
public static readonly Guid DesktopWallpaper = new("C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ public static partial class Ole32
|
||||
CLSCTX dwClsContext,
|
||||
ref Guid riid,
|
||||
out IntPtr rReturnedComObject);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
internal static partial int CoInitializeEx(nint pvReserved, uint dwCoInit);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
internal static partial void CoUninitialize();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for recording the screen during UI tests.
|
||||
/// Requires FFmpeg to be installed and available in PATH.
|
||||
/// </summary>
|
||||
internal class ScreenRecording : IDisposable
|
||||
{
|
||||
private readonly string outputDirectory;
|
||||
private readonly string framesDirectory;
|
||||
private readonly string outputFilePath;
|
||||
private readonly List<string> capturedFrames;
|
||||
private readonly SemaphoreSlim recordingLock = new(1, 1);
|
||||
private readonly Stopwatch recordingStopwatch = new();
|
||||
private readonly string? ffmpegPath;
|
||||
private CancellationTokenSource? recordingCancellation;
|
||||
private Task? recordingTask;
|
||||
private bool isRecording;
|
||||
private int frameCount;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
|
||||
|
||||
private const int CURSORSHOWING = 0x00000001;
|
||||
private const int DESKTOPHORZRES = 118;
|
||||
private const int DESKTOPVERTRES = 117;
|
||||
private const int DINORMAL = 0x0003;
|
||||
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
|
||||
public ScreenRecording(string outputDirectory)
|
||||
{
|
||||
this.outputDirectory = outputDirectory;
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
|
||||
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
|
||||
capturedFrames = new List<string>();
|
||||
frameCount = 0;
|
||||
|
||||
// Check if FFmpeg is available
|
||||
ffmpegPath = FindFfmpeg();
|
||||
if (ffmpegPath == null)
|
||||
{
|
||||
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
|
||||
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether screen recording is available (FFmpeg found).
|
||||
/// </summary>
|
||||
public bool IsAvailable => ffmpegPath != null;
|
||||
|
||||
/// <summary>
|
||||
/// Starts recording the screen.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StartRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (isRecording || !IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create frames directory
|
||||
Directory.CreateDirectory(framesDirectory);
|
||||
|
||||
recordingCancellation = new CancellationTokenSource();
|
||||
isRecording = true;
|
||||
recordingStopwatch.Start();
|
||||
|
||||
// Start the recording task
|
||||
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
|
||||
|
||||
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start recording: {ex.Message}");
|
||||
isRecording = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops recording and encodes video.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StopRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!isRecording || recordingCancellation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal cancellation
|
||||
recordingCancellation.Cancel();
|
||||
|
||||
// Wait for recording task to complete
|
||||
if (recordingTask != null)
|
||||
{
|
||||
await recordingTask;
|
||||
}
|
||||
|
||||
recordingStopwatch.Stop();
|
||||
isRecording = false;
|
||||
|
||||
double duration = recordingStopwatch.Elapsed.TotalSeconds;
|
||||
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
|
||||
|
||||
// Encode to video
|
||||
await EncodeToVideoAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error stopping recording: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup();
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records frames from the screen.
|
||||
/// </summary>
|
||||
private void RecordFrames(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
int frameInterval = 1000 / TargetFps;
|
||||
var frameTimer = Stopwatch.StartNew();
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var frameStart = frameTimer.ElapsedMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
CaptureFrame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error capturing frame: {ex.Message}");
|
||||
}
|
||||
|
||||
// Sleep for remaining time to maintain target FPS
|
||||
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
|
||||
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
|
||||
|
||||
if (sleepTime > 0)
|
||||
{
|
||||
Thread.Sleep(sleepTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error during recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a single frame.
|
||||
/// </summary>
|
||||
private void CaptureFrame()
|
||||
{
|
||||
IntPtr hdc = GetDC(IntPtr.Zero);
|
||||
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
|
||||
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
|
||||
ReleaseDC(IntPtr.Zero, hdc);
|
||||
|
||||
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
|
||||
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
|
||||
{
|
||||
using (Graphics g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
|
||||
|
||||
ScreenCapture.CURSORINFO cursorInfo;
|
||||
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
|
||||
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
|
||||
{
|
||||
IntPtr hdcDest = g.GetHdc();
|
||||
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
|
||||
g.ReleaseHdc(hdcDest);
|
||||
}
|
||||
}
|
||||
|
||||
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
|
||||
bitmap.Save(framePath, ImageFormat.Jpeg);
|
||||
capturedFrames.Add(framePath);
|
||||
frameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes captured frames to video using ffmpeg.
|
||||
/// </summary>
|
||||
private async Task EncodeToVideoAsync()
|
||||
{
|
||||
if (capturedFrames.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No frames captured");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Build ffmpeg command with proper non-interactive flags
|
||||
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
|
||||
|
||||
// -y: overwrite without asking
|
||||
// -nostdin: disable interaction
|
||||
// -loglevel error: only show errors
|
||||
// -stats: show encoding progress
|
||||
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
|
||||
|
||||
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ffmpegPath!,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process != null)
|
||||
{
|
||||
// Close stdin immediately to ensure FFmpeg doesn't wait for input
|
||||
process.StandardInput.Close();
|
||||
|
||||
// Read output streams asynchronously to prevent deadlock
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
// Wait for process to exit
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
// Get the output
|
||||
string stdout = await outputTask;
|
||||
string stderr = await errorTask;
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(outputFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(outputFilePath);
|
||||
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
|
||||
if (!string.IsNullOrWhiteSpace(stderr))
|
||||
{
|
||||
Console.WriteLine($"FFmpeg error: {stderr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error encoding video: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds ffmpeg executable.
|
||||
/// </summary>
|
||||
private static string? FindFfmpeg()
|
||||
{
|
||||
// Check if ffmpeg is in PATH
|
||||
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
|
||||
|
||||
foreach (var dir in pathDirs)
|
||||
{
|
||||
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
|
||||
if (File.Exists(ffmpegPath))
|
||||
{
|
||||
return ffmpegPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Check common installation locations
|
||||
var commonPaths = new[]
|
||||
{
|
||||
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
|
||||
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
|
||||
};
|
||||
|
||||
foreach (var path in commonPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the recorded video file.
|
||||
/// </summary>
|
||||
public string OutputFilePath => outputFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory containing recordings.
|
||||
/// </summary>
|
||||
public string OutputDirectory => outputDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up resources.
|
||||
/// </summary>
|
||||
private void Cleanup()
|
||||
{
|
||||
recordingCancellation?.Dispose();
|
||||
recordingCancellation = null;
|
||||
recordingTask = null;
|
||||
|
||||
// Clean up frames directory if it exists
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(framesDirectory))
|
||||
{
|
||||
Directory.Delete(framesDirectory, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
recordingLock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
/// <param name="args">Optional command line arguments to pass to the application.</param>
|
||||
public void StartExe(string appPath, string[]? args = null)
|
||||
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
if (!string.IsNullOrEmpty(enableModules))
|
||||
{
|
||||
opts.AddAdditionalCapability("enableModules", enableModules);
|
||||
}
|
||||
|
||||
if (scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
private void TryLaunchPowerToysSettings(AppiumOptions opts)
|
||||
{
|
||||
try
|
||||
if (opts.ToCapabilities().HasCapability("enableModules"))
|
||||
{
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
|
||||
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings();
|
||||
}
|
||||
|
||||
const int maxTries = 3;
|
||||
const int delayMs = 5000;
|
||||
const int maxRetries = 3;
|
||||
|
||||
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
|
||||
// Verify process was killed
|
||||
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
|
||||
var remainingProcesses = Process.GetProcessesByName(exeName);
|
||||
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
|
||||
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
|
||||
{
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
return;
|
||||
}
|
||||
|
||||
// Window not found, kill all PowerToys processes and retry
|
||||
if (tryCount < maxTries)
|
||||
{
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (tryCount == maxTries)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
// Kill processes and retry
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
|
||||
}
|
||||
|
||||
private void TryLaunchCommandPalette(AppiumOptions opts)
|
||||
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
|
||||
var process = Process.Start(processStartInfo);
|
||||
process?.WaitForExit();
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
|
||||
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
|
||||
{
|
||||
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
var hexHwnd = window[0].HWnd.ToString("x");
|
||||
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exceptions if needed
|
||||
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts now exe and takes control of it.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public void RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
ExitScopeExe();
|
||||
StartExe(locationPath + sessionPath, this.commandLineArgs);
|
||||
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
|
||||
}
|
||||
|
||||
public WindowsDriver<WindowsElement> GetRoot()
|
||||
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
|
||||
this.ExitExe(winAppDriverProcessInfo.FileName);
|
||||
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
}
|
||||
|
||||
private void KillPowerToysProcesses()
|
||||
{
|
||||
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
|
||||
|
||||
foreach (var processName in powerToysProcessNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
// Verify processes are actually gone
|
||||
var remainingProcesses = Process.GetProcessesByName(processName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,18 @@ namespace Microsoft.PowerToys.UITest
|
||||
public class SettingsConfigHelper
|
||||
{
|
||||
private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true };
|
||||
private static readonly SettingsUtils SettingsUtils = new SettingsUtils();
|
||||
private static readonly SettingsUtils SettingsUtils = SettingsUtils.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Configures global PowerToys settings to enable only specified modules and disable all others.
|
||||
/// </summary>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
|
||||
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
|
||||
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modulesToEnable);
|
||||
modulesToEnable ??= Array.Empty<string>();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public string? ScreenshotDirectory { get; set; }
|
||||
|
||||
public string? RecordingDirectory { get; set; }
|
||||
|
||||
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
|
||||
|
||||
private readonly PowerToysModule scope;
|
||||
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
private readonly string[]? commandLineArgs;
|
||||
private SessionHelper? sessionHelper;
|
||||
private System.Threading.Timer? screenshotTimer;
|
||||
private ScreenRecording? screenRecording;
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
|
||||
{
|
||||
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
|
||||
CloseOtherApplications();
|
||||
if (IsInPipeline)
|
||||
{
|
||||
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
|
||||
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(ScreenshotDirectory);
|
||||
|
||||
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(RecordingDirectory);
|
||||
|
||||
// Take screenshot every 1 second
|
||||
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
|
||||
|
||||
// Start screen recording (requires FFmpeg)
|
||||
try
|
||||
{
|
||||
screenRecording = new ScreenRecording(RecordingDirectory);
|
||||
if (screenRecording.IsAvailable)
|
||||
{
|
||||
_ = screenRecording.StartRecordingAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
screenRecording = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
|
||||
screenRecording = null;
|
||||
}
|
||||
|
||||
// Escape Popups before starting
|
||||
System.Windows.Forms.SendKeys.SendWait("{ESC}");
|
||||
}
|
||||
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
|
||||
if (IsInPipeline)
|
||||
{
|
||||
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Dispose();
|
||||
|
||||
// Stop screen recording
|
||||
if (screenRecording != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
|
||||
or UnitTestOutcome.Error
|
||||
or UnitTestOutcome.Unknown)
|
||||
{
|
||||
Task.Delay(1000).Wait();
|
||||
AddScreenShotsToTestResultsDirectory();
|
||||
AddRecordingsToTestResultsDirectory();
|
||||
AddLogFilesToTestResultsDirectory();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clean up recording if test passed
|
||||
CleanupRecordingDirectory();
|
||||
}
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
this.Session.Cleanup();
|
||||
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
public void Dispose()
|
||||
{
|
||||
screenshotTimer?.Dispose();
|
||||
screenRecording?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds screen recordings to test results directory when test fails.
|
||||
/// </summary>
|
||||
protected void AddRecordingsToTestResultsDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
// Add video files (MP4)
|
||||
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
|
||||
foreach (string file in videoFiles)
|
||||
{
|
||||
this.TestContext.AddResultFile(file);
|
||||
var fileInfo = new FileInfo(file);
|
||||
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
|
||||
if (videoFiles.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up recording directory when test passes.
|
||||
/// </summary>
|
||||
private void CleanupRecordingDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(RecordingDirectory, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies PowerToys log files to test results directory when test fails.
|
||||
/// Renames files to include the directory structure after \PowerToys.
|
||||
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Restart scope exe.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public Session RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
this.sessionHelper!.RestartScopeExe();
|
||||
this.sessionHelper!.RestartScopeExe(enableModules);
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
|
||||
return;
|
||||
return Session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,9 +16,54 @@
|
||||
|
||||
namespace registry
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
struct on_exit
|
||||
{
|
||||
std::function<void()> f;
|
||||
|
||||
on_exit(std::function<void()> f) :
|
||||
f{ std::move(f) } {}
|
||||
~on_exit() { f(); }
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
inline const wchar_t* getScopeName(HKEY scope)
|
||||
{
|
||||
if (scope == HKEY_LOCAL_MACHINE)
|
||||
{
|
||||
return L"HKLM";
|
||||
}
|
||||
else if (scope == HKEY_CURRENT_USER)
|
||||
{
|
||||
return L"HKCU";
|
||||
}
|
||||
else if (scope == HKEY_CLASSES_ROOT)
|
||||
{
|
||||
return L"HKCR";
|
||||
}
|
||||
else
|
||||
{
|
||||
return L"HK??";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace install_scope
|
||||
{
|
||||
const wchar_t INSTALL_SCOPE_REG_KEY[] = L"Software\\Classes\\powertoys\\";
|
||||
const wchar_t UNINSTALL_REG_KEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
|
||||
|
||||
// Bundle UpgradeCode from PowerToys.wxs (with braces as stored in registry)
|
||||
const wchar_t BUNDLE_UPGRADE_CODE[] = L"{6341382D-C0A9-4238-9188-BE9607E3FAB2}";
|
||||
|
||||
enum class InstallScope
|
||||
{
|
||||
@@ -26,8 +71,67 @@ namespace registry
|
||||
PerUser,
|
||||
};
|
||||
|
||||
// Helper function to find PowerToys bundle in Windows Uninstall registry by BundleUpgradeCode
|
||||
inline bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey)
|
||||
{
|
||||
HKEY uninstallKey{};
|
||||
if (RegOpenKeyExW(rootKey, UNINSTALL_REG_KEY, 0, KEY_READ, &uninstallKey) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeUninstallKey{ [uninstallKey] { RegCloseKey(uninstallKey); } };
|
||||
|
||||
DWORD index = 0;
|
||||
wchar_t subKeyName[256];
|
||||
|
||||
// Enumerate all subkeys under Uninstall
|
||||
while (RegEnumKeyW(uninstallKey, index++, subKeyName, 256) == ERROR_SUCCESS)
|
||||
{
|
||||
HKEY productKey{};
|
||||
if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ, &productKey) != ERROR_SUCCESS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
detail::on_exit closeProductKey{ [productKey] { RegCloseKey(productKey); } };
|
||||
|
||||
// Check BundleUpgradeCode value (specific to WiX Bundle installations)
|
||||
wchar_t bundleUpgradeCode[256]{};
|
||||
DWORD bundleUpgradeCodeSize = sizeof(bundleUpgradeCode);
|
||||
|
||||
if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, nullptr,
|
||||
reinterpret_cast<LPBYTE>(bundleUpgradeCode), &bundleUpgradeCodeSize) == ERROR_SUCCESS)
|
||||
{
|
||||
if (_wcsicmp(bundleUpgradeCode, BUNDLE_UPGRADE_CODE) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline const InstallScope get_current_install_scope()
|
||||
{
|
||||
// 1. Check HKCU Uninstall registry first (user-level bundle)
|
||||
// Note: MSI components are always in HKLM regardless of install scope,
|
||||
// but the Bundle entry will be in HKCU for per-user installations
|
||||
if (find_powertoys_bundle_in_uninstall_registry(HKEY_CURRENT_USER))
|
||||
{
|
||||
Logger::info(L"Found user-level PowerToys bundle via BundleUpgradeCode in HKCU");
|
||||
return InstallScope::PerUser;
|
||||
}
|
||||
|
||||
// 2. Check HKLM Uninstall registry (machine-level bundle)
|
||||
if (find_powertoys_bundle_in_uninstall_registry(HKEY_LOCAL_MACHINE))
|
||||
{
|
||||
Logger::info(L"Found machine-level PowerToys bundle via BundleUpgradeCode in HKLM");
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
|
||||
// 3. Fallback to legacy custom registry key detection
|
||||
Logger::info(L"PowerToys bundle not found in Uninstall registry, falling back to legacy detection");
|
||||
|
||||
// Open HKLM key
|
||||
HKEY perMachineKey{};
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||
@@ -45,6 +149,7 @@ namespace registry
|
||||
&perUserKey) != ERROR_SUCCESS)
|
||||
{
|
||||
// both keys are missing
|
||||
Logger::warn(L"No PowerToys installation detected, defaulting to PerMachine");
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
else
|
||||
@@ -96,47 +201,6 @@ namespace registry
|
||||
template<class>
|
||||
inline constexpr bool always_false_v = false;
|
||||
|
||||
namespace detail
|
||||
{
|
||||
struct on_exit
|
||||
{
|
||||
std::function<void()> f;
|
||||
|
||||
on_exit(std::function<void()> f) :
|
||||
f{ std::move(f) } {}
|
||||
~on_exit() { f(); }
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
inline const wchar_t* getScopeName(HKEY scope)
|
||||
{
|
||||
if (scope == HKEY_LOCAL_MACHINE)
|
||||
{
|
||||
return L"HKLM";
|
||||
}
|
||||
else if (scope == HKEY_CURRENT_USER)
|
||||
{
|
||||
return L"HKCU";
|
||||
}
|
||||
else if (scope == HKEY_CLASSES_ROOT)
|
||||
{
|
||||
return L"HKCR";
|
||||
}
|
||||
else
|
||||
{
|
||||
return L"HK??";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ValueChange
|
||||
{
|
||||
using value_t = std::variant<DWORD, std::wstring>;
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
|
||||
public abstract class SettingsResourceModuleTest<TSettingsConfig> : BaseDscTest
|
||||
where TSettingsConfig : ISettingsConfig, new()
|
||||
{
|
||||
private readonly SettingsUtils _settingsUtils = new();
|
||||
private readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
|
||||
private TSettingsConfig _originalSettings;
|
||||
|
||||
protected TSettingsConfig DefaultSettings => new();
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace PowerToys.DSC.Models.FunctionData;
|
||||
public sealed class SettingsFunctionData<TSettingsConfig> : BaseFunctionData, ISettingsFunctionData
|
||||
where TSettingsConfig : ISettingsConfig, new()
|
||||
{
|
||||
private static readonly SettingsUtils _settingsUtils = new();
|
||||
private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
|
||||
private static readonly TSettingsConfig _settingsConfig = new();
|
||||
|
||||
private readonly SettingsResourceObject<TSettingsConfig> _input;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using AdvancedPaste.Converters;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.UI;
|
||||
|
||||
namespace AdvancedPaste.UnitTests.ConvertersTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class HexColorToColorConverterTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestConvert_ValidSixDigitHex_ReturnsColor()
|
||||
{
|
||||
Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#FFBFAB");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var color = (Windows.UI.Color)result;
|
||||
Assert.AreEqual(255, color.R);
|
||||
Assert.AreEqual(191, color.G);
|
||||
Assert.AreEqual(171, color.B);
|
||||
Assert.AreEqual(255, color.A);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestConvert_ValidThreeDigitHex_ReturnsColor()
|
||||
{
|
||||
Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#abc");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var color = (Windows.UI.Color)result;
|
||||
|
||||
// #abc should expand to #aabbcc
|
||||
Assert.AreEqual(170, color.R); // 0xaa
|
||||
Assert.AreEqual(187, color.G); // 0xbb
|
||||
Assert.AreEqual(204, color.B); // 0xcc
|
||||
Assert.AreEqual(255, color.A);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestConvert_NullOrEmpty_ReturnsNull()
|
||||
{
|
||||
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(null));
|
||||
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(string.Empty));
|
||||
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(" "));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestConvert_InvalidHex_ReturnsNull()
|
||||
{
|
||||
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#GGGGGG"));
|
||||
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#12345"));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace AdvancedPaste.UnitTests.HelpersTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class ClipboardItemHelperTests
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow("#FFBFAB", true)]
|
||||
[DataRow("#000000", true)]
|
||||
[DataRow("#FFFFFF", true)]
|
||||
[DataRow("#fff", true)]
|
||||
[DataRow("#abc", true)]
|
||||
[DataRow("#123456", true)]
|
||||
[DataRow("#AbCdEf", true)]
|
||||
[DataRow("FFBFAB", false)] // Missing #
|
||||
[DataRow("#GGGGGG", false)] // Invalid hex characters
|
||||
[DataRow("#12345", false)] // Wrong length
|
||||
[DataRow("#1234567", false)] // Too long
|
||||
[DataRow("", false)]
|
||||
[DataRow(null, false)]
|
||||
[DataRow(" #FFF ", true)] // Whitespace should be trimmed
|
||||
[DataRow("Not a color", false)]
|
||||
[DataRow("#", false)]
|
||||
[DataRow("##FFFFFF", false)]
|
||||
public void TestIsRgbHexColor(string input, bool expected)
|
||||
{
|
||||
bool result = ClipboardItemHelper.IsRgbHexColor(input);
|
||||
Assert.AreEqual(expected, result, $"IsRgbHexColor(\"{input}\") should return {expected}");
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeToFriendlyStringConverter x:Key="DateTimeToFriendlyStringConverter" />
|
||||
<converters:HexColorToBrushConverter x:Key="HexColorToBrushConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid ColumnSpacing="12">
|
||||
@@ -25,6 +26,26 @@
|
||||
Source="{x:Bind ClipboardItem.Image, Mode=OneWay}"
|
||||
Stretch="UniformToFill"
|
||||
Visibility="{x:Bind HasImage, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<!-- Color preview with text -->
|
||||
<Grid Visibility="{x:Bind HasColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Ellipse
|
||||
Grid.Column="0"
|
||||
Width="8"
|
||||
Height="8"
|
||||
Margin="8,0,8,0"
|
||||
Fill="{x:Bind ClipboardItem.Content, Mode=OneWay, Converter={StaticResource HexColorToBrushConverter}}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ClipboardItem.Content, Mode=OneWay}"
|
||||
TextWrapping="NoWrap" />
|
||||
</Grid>
|
||||
<!-- Text preview -->
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
|
||||
@@ -38,9 +38,11 @@ namespace AdvancedPaste.Controls
|
||||
|
||||
public bool HasImage => ContentImage is not null;
|
||||
|
||||
public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage;
|
||||
public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage && !HasColor;
|
||||
|
||||
public bool HasGlyph => !HasImage && !HasText && !string.IsNullOrEmpty(IconGlyph);
|
||||
public bool HasGlyph => !HasImage && !HasText && !HasColor && !string.IsNullOrEmpty(IconGlyph);
|
||||
|
||||
public bool HasColor => ClipboardItemHelper.IsRgbHexColor(ContentText);
|
||||
|
||||
public ClipboardHistoryItemPreviewControl()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace AdvancedPaste.Converters
|
||||
{
|
||||
public static class HexColorConverterHelper
|
||||
{
|
||||
public static Windows.UI.Color? ConvertHexColorToRgb(string hexColor)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Remove # if present
|
||||
var cleanHex = hexColor.TrimStart('#');
|
||||
|
||||
// Expand 3-digit hex to 6-digit (#ABC -> #AABBCC)
|
||||
if (cleanHex.Length == 3)
|
||||
{
|
||||
cleanHex = $"{cleanHex[0]}{cleanHex[0]}{cleanHex[1]}{cleanHex[1]}{cleanHex[2]}{cleanHex[2]}";
|
||||
}
|
||||
|
||||
if (cleanHex.Length == 6)
|
||||
{
|
||||
var r = System.Convert.ToByte(cleanHex.Substring(0, 2), 16);
|
||||
var g = System.Convert.ToByte(cleanHex.Substring(2, 2), 16);
|
||||
var b = System.Convert.ToByte(cleanHex.Substring(4, 2), 16);
|
||||
|
||||
return Windows.UI.Color.FromArgb(255, r, g, b);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Invalid color format - return null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace AdvancedPaste.Converters
|
||||
{
|
||||
public sealed partial class HexColorToBrushConverter : IValueConverter
|
||||
{
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is not string hexColor || string.IsNullOrWhiteSpace(hexColor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Windows.UI.Color? color = HexColorConverterHelper.ConvertHexColorToRgb(hexColor);
|
||||
|
||||
return color != null ? new SolidColorBrush((Windows.UI.Color)color) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,20 +198,14 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
private void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
{
|
||||
if (args.InvokedItem is ClipboardItem item)
|
||||
if (args.InvokedItem is ClipboardItem item && item.Item is not null)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
|
||||
if (!string.IsNullOrEmpty(item.Content))
|
||||
{
|
||||
ClipboardHelper.SetTextContent(item.Content);
|
||||
}
|
||||
else if (item.Image is not null)
|
||||
{
|
||||
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
|
||||
ClipboardHelper.SetImageContent(image);
|
||||
}
|
||||
|
||||
// Use SetHistoryItemAsContent to set the clipboard content without creating a new history entry
|
||||
Clipboard.SetHistoryItemAsContent(item.Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
@@ -10,8 +11,11 @@ using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
{
|
||||
internal static class ClipboardItemHelper
|
||||
internal static partial class ClipboardItemHelper
|
||||
{
|
||||
// Compiled regex for better performance when checking multiple clipboard items
|
||||
private static readonly Regex HexColorRegex = HexColorCompiledRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ClipboardItem from current clipboard data.
|
||||
/// </summary>
|
||||
@@ -55,6 +59,31 @@ namespace AdvancedPaste.Helpers
|
||||
return clipboardItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if text is a valid RGB hex color (e.g., #FFBFAB or #fff).
|
||||
/// </summary>
|
||||
public static bool IsRgbHexColor(string text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string trimmedText = text.Trim();
|
||||
if (trimmedText.Length > 7)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trimmedText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match #RGB or #RRGGBB format (case-insensitive)
|
||||
return HexColorRegex.IsMatch(trimmedText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BitmapImage from clipboard data.
|
||||
/// </summary>
|
||||
@@ -80,5 +109,8 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")]
|
||||
private static partial Regex HexColorCompiledRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Hosts.Settings
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
var defaultSettings = new HostsProperties();
|
||||
ShowStartupWarning = defaultSettings.ShowStartupWarning;
|
||||
LoopbackDuplicates = defaultSettings.LoopbackDuplicates;
|
||||
|
||||
@@ -50,6 +50,7 @@ enum class ScheduleMode
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise,
|
||||
FollowNightLight,
|
||||
// add more later
|
||||
};
|
||||
|
||||
@@ -61,6 +62,8 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FixedHours:
|
||||
return L"FixedHours";
|
||||
case ScheduleMode::FollowNightLight:
|
||||
return L"FollowNightLight";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
@@ -72,6 +75,8 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FollowNightLight")
|
||||
return ScheduleMode::FollowNightLight;
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
@@ -167,7 +172,9 @@ public:
|
||||
ToString(g_settings.m_scheduleMode),
|
||||
{ { L"Off", L"Disable the schedule" },
|
||||
{ L"FixedHours", L"Set hours manually" },
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" },
|
||||
{ L"FollowNightLight", L"Follow Windows Night Light state" }
|
||||
});
|
||||
|
||||
// Integer spinners
|
||||
settings.add_int_spinner(
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
#include <utils/logger_helper.h>
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <LightSwitchUtils.h>
|
||||
#include <NightLightRegistryObserver.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
HANDLE g_ServiceStopEvent = nullptr;
|
||||
static LightSwitchStateManager* g_stateManagerPtr = nullptr;
|
||||
|
||||
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
|
||||
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
|
||||
@@ -168,7 +170,15 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan
|
||||
}
|
||||
|
||||
// Use shared helper (handles wraparound logic)
|
||||
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
bool shouldBeLight = false;
|
||||
if (s.scheduleMode == ScheduleMode::FollowNightLight)
|
||||
{
|
||||
shouldBeLight = !IsNightLightEnabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
}
|
||||
|
||||
// Compare current system/apps theme
|
||||
bool currentSystemLight = GetCurrentSystemTheme();
|
||||
@@ -199,15 +209,40 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
// Initialization
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
static LightSwitchStateManager stateManager;
|
||||
g_stateManagerPtr = &stateManager;
|
||||
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
|
||||
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
static std::unique_ptr<NightLightRegistryObserver> g_nightLightWatcher;
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// after loading settings:
|
||||
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
|
||||
|
||||
if (nightLightNeeded && !g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
|
||||
|
||||
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
|
||||
HKEY_CURRENT_USER,
|
||||
NIGHT_LIGHT_REGISTRY_PATH,
|
||||
[]() {
|
||||
if (g_stateManagerPtr)
|
||||
g_stateManagerPtr->OnNightLightChange();
|
||||
});
|
||||
}
|
||||
else if (!nightLightNeeded && g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
@@ -274,6 +309,31 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
ResetEvent(hSettingsChanged);
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
stateManager.OnSettingsChanged();
|
||||
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight);
|
||||
|
||||
if (nightLightNeeded && !g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Starting Night Light registry watcher...");
|
||||
|
||||
g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>(
|
||||
HKEY_CURRENT_USER,
|
||||
NIGHT_LIGHT_REGISTRY_PATH,
|
||||
[]() {
|
||||
if (g_stateManagerPtr)
|
||||
g_stateManagerPtr->OnNightLightChange();
|
||||
});
|
||||
|
||||
stateManager.OnNightLightChange();
|
||||
}
|
||||
else if (!nightLightNeeded && g_nightLightWatcher)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher...");
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -285,6 +345,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
CloseHandle(hManualOverride);
|
||||
if (hParent)
|
||||
CloseHandle(hParent);
|
||||
if (g_nightLightWatcher)
|
||||
{
|
||||
g_nightLightWatcher->Stop();
|
||||
g_nightLightWatcher.reset();
|
||||
}
|
||||
|
||||
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
|
||||
return 0;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
@@ -88,6 +89,7 @@
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="LightSwitchStateManager.h" />
|
||||
<ClInclude Include="LightSwitchUtils.h" />
|
||||
<ClInclude Include="NightLightRegistryObserver.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<ClCompile Include="LightSwitchStateManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
@@ -62,6 +65,9 @@
|
||||
<ClInclude Include="LightSwitchUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="NightLightRegistryObserver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
|
||||
@@ -19,7 +19,8 @@ enum class ScheduleMode
|
||||
{
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise
|
||||
SunsetToSunrise,
|
||||
FollowNightLight,
|
||||
// Add more in the future
|
||||
};
|
||||
|
||||
@@ -31,6 +32,8 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
return L"FixedHours";
|
||||
case ScheduleMode::SunsetToSunrise:
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FollowNightLight:
|
||||
return L"FollowNightLight";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
@@ -42,6 +45,8 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FollowNightLight")
|
||||
return ScheduleMode::FollowNightLight;
|
||||
else
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ void LightSwitchStateManager::OnSettingsChanged()
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
EvaluateAndApplyIfNeeded();
|
||||
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
|
||||
{
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
// Called when manual override is triggered
|
||||
@@ -49,8 +52,38 @@ void LightSwitchStateManager::OnManualOverride()
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Runs with the registry observer detects a change in Night Light settings.
|
||||
void LightSwitchStateManager::OnNightLightChange()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
|
||||
bool newNightLightState = IsNightLightEnabled();
|
||||
|
||||
// In Follow Night Light mode, treat a Night Light toggle as a boundary
|
||||
if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; "
|
||||
L"treating as a boundary and clearing manual override.");
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
|
||||
if (newNightLightState != _state.isNightLightActive)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}",
|
||||
newNightLightState ? L"ON" : L"OFF");
|
||||
|
||||
_state.isNightLightActive = newNightLightState;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change.");
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
@@ -77,9 +110,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
static std::pair<int, int> update_sun_times(auto& settings)
|
||||
@@ -194,7 +227,15 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.lastAppliedMode = _currentSettings.scheduleMode;
|
||||
|
||||
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
bool shouldBeLight = false;
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight)
|
||||
{
|
||||
shouldBeLight = !_state.isNightLightActive;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
}
|
||||
|
||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||
@@ -227,6 +268,3 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ struct LightSwitchState
|
||||
bool isManualOverride = false;
|
||||
bool isSystemLightActive = false;
|
||||
bool isAppsLightActive = false;
|
||||
bool isNightLightActive = false;
|
||||
int lastEvaluatedDay = -1;
|
||||
int lastTickMinutes = -1;
|
||||
|
||||
@@ -32,6 +33,9 @@ public:
|
||||
// Called when manual override is toggled (via shortcut or system change).
|
||||
void OnManualOverride();
|
||||
|
||||
// Called when night light changes in windows settings
|
||||
void OnNightLightChange();
|
||||
|
||||
// Initial sync at startup to align internal state with system theme
|
||||
void SyncInitialThemeState();
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#include "NightLightRegistryObserver.h"
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
#include <wtypes.h>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
class NightLightRegistryObserver
|
||||
{
|
||||
public:
|
||||
NightLightRegistryObserver(HKEY root, const std::wstring& subkey, std::function<void()> callback) :
|
||||
_root(root), _subkey(subkey), _callback(std::move(callback)), _stop(false)
|
||||
{
|
||||
_thread = std::thread([this]() { this->Run(); });
|
||||
}
|
||||
|
||||
~NightLightRegistryObserver()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
_stop = true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_event)
|
||||
SetEvent(_event);
|
||||
}
|
||||
|
||||
if (_thread.joinable())
|
||||
_thread.join();
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_hKey)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
}
|
||||
|
||||
if (_event)
|
||||
{
|
||||
CloseHandle(_event);
|
||||
_event = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
void Run()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (RegOpenKeyExW(_root, _subkey.c_str(), 0, KEY_NOTIFY, &_hKey) != ERROR_SUCCESS)
|
||||
return;
|
||||
|
||||
_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!_event)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (!_stop)
|
||||
{
|
||||
HKEY hKeyLocal = nullptr;
|
||||
HANDLE eventLocal = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_stop)
|
||||
break;
|
||||
|
||||
hKeyLocal = _hKey;
|
||||
eventLocal = _event;
|
||||
}
|
||||
|
||||
if (!hKeyLocal || !eventLocal)
|
||||
break;
|
||||
|
||||
if (_stop)
|
||||
break;
|
||||
|
||||
if (RegNotifyChangeKeyValue(hKeyLocal, FALSE, REG_NOTIFY_CHANGE_LAST_SET, eventLocal, TRUE) != ERROR_SUCCESS)
|
||||
break;
|
||||
|
||||
DWORD wait = WaitForSingleObject(eventLocal, INFINITE);
|
||||
if (_stop || wait == WAIT_FAILED)
|
||||
break;
|
||||
|
||||
ResetEvent(eventLocal);
|
||||
|
||||
if (!_stop && _callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
_callback();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_hKey)
|
||||
{
|
||||
RegCloseKey(_hKey);
|
||||
_hKey = nullptr;
|
||||
}
|
||||
|
||||
if (_event)
|
||||
{
|
||||
CloseHandle(_event);
|
||||
_event = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HKEY _root;
|
||||
std::wstring _subkey;
|
||||
std::function<void()> _callback;
|
||||
HANDLE _event = nullptr;
|
||||
HKEY _hKey = nullptr;
|
||||
std::thread _thread;
|
||||
std::atomic<bool> _stop;
|
||||
std::mutex _mutex;
|
||||
};
|
||||
@@ -11,4 +11,7 @@ enum class SettingId
|
||||
Sunset_Offset,
|
||||
ChangeSystem,
|
||||
ChangeApps
|
||||
};
|
||||
};
|
||||
|
||||
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "ThemeHelper.h"
|
||||
#include <SettingsConstants.h>
|
||||
|
||||
// Controls changing the themes.
|
||||
|
||||
@@ -10,7 +11,7 @@ static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -31,7 +32,7 @@ void SetAppsTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -50,7 +51,7 @@ void SetSystemTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -79,7 +80,7 @@ bool GetCurrentSystemTheme()
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -98,7 +99,7 @@ bool GetCurrentAppsTheme()
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
PERSONALIZATION_REGISTRY_PATH,
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
@@ -109,3 +110,30 @@ bool GetCurrentAppsTheme()
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
|
||||
bool IsNightLightEnabled()
|
||||
{
|
||||
HKEY hKey;
|
||||
const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH;
|
||||
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
|
||||
return false;
|
||||
|
||||
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
|
||||
DWORD size = 0;
|
||||
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BYTE> data(size);
|
||||
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
return data[23] == 0x10 && data[24] == 0x00;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
@@ -11,7 +11,7 @@ namespace MeasureToolUI
|
||||
{
|
||||
public sealed class Settings
|
||||
{
|
||||
private static readonly SettingsUtils ModuleSettings = new();
|
||||
private static readonly SettingsUtils ModuleSettings = SettingsUtils.Default;
|
||||
|
||||
public MeasureToolMeasureStyle DefaultMeasureStyle
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ internal sealed class SettingsHelper
|
||||
lock (this.LockObject)
|
||||
{
|
||||
{
|
||||
var settingsUtils = new SettingsUtils();
|
||||
var settingsUtils = SettingsUtils.Default;
|
||||
|
||||
// set this to 1 to disable retries
|
||||
var remainingRetries = 5;
|
||||
|
||||
@@ -617,6 +617,8 @@ namespace MouseUtils.UITests
|
||||
|
||||
private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false)
|
||||
{
|
||||
Session = RestartScopeExe("FindMyMouse,MouseHighlighter,MouseJump,MousePointerCrosshairs,CursorWrap");
|
||||
|
||||
// this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ namespace MouseWithoutBorders.Class
|
||||
|
||||
internal Settings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
|
||||
_watcher = SettingsHelper.GetFileWatcher("MouseWithoutBorders", "settings.json", () =>
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace PowerOCR.Settings
|
||||
[ImportingConstructor]
|
||||
public UserSettings(Helpers.IThrottledActionInvoker throttledActionInvoker)
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
ActivationShortcut = new SettingItem<string>(DefaultActivationShortcut);
|
||||
PreferredLanguage = new SettingItem<string>(string.Empty);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace WorkspacesEditor.Utils
|
||||
public class Settings
|
||||
{
|
||||
private const string WorkspacesModuleName = "Workspaces";
|
||||
private static readonly SettingsUtils _settingsUtils = new();
|
||||
private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
|
||||
|
||||
public static WorkspacesSettings ReadSettings()
|
||||
{
|
||||
|
||||
@@ -133,7 +133,7 @@ namespace WorkspacesEditor.ViewModels
|
||||
_orderByIndex = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
settings.Properties.SortBy = (WorkspacesProperties.SortByProperty)value;
|
||||
settings.Save(new SettingsUtils());
|
||||
settings.Save(SettingsUtils.Default);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Awake.Core
|
||||
{
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_stateQueue = [];
|
||||
ModuleSettings = new SettingsUtils();
|
||||
ModuleSettings = SettingsUtils.Default;
|
||||
}
|
||||
|
||||
internal static void StartMonitor()
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Awake
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
|
||||
LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
@@ -16,6 +17,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||
|
||||
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||
|
||||
@@ -65,6 +68,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
{
|
||||
get
|
||||
@@ -157,6 +162,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// will never be able to load Hotkeys & aliases
|
||||
UpdateProperty(nameof(IsInitialized));
|
||||
|
||||
if (model is IExtendedAttributesProvider extendedAttributesProvider)
|
||||
{
|
||||
ExtendedAttributesProvider = new ExtensionObject<IExtendedAttributesProvider>(extendedAttributesProvider);
|
||||
var properties = extendedAttributesProvider.GetProperties();
|
||||
UpdateDataPackage(properties);
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.Initialized;
|
||||
}
|
||||
|
||||
@@ -379,6 +391,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
|
||||
break;
|
||||
case nameof(DataPackage):
|
||||
UpdateDataPackage(ExtendedAttributesProvider?.Unsafe?.GetProperties());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -431,6 +446,16 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
private void UpdateDataPackage(IDictionary<string, object?>? properties)
|
||||
{
|
||||
DataPackage =
|
||||
properties?.TryGetValue(WellKnownExtensionAttributes.DataPackage, out var dataPackageView) == true &&
|
||||
dataPackageView is DataPackageView view
|
||||
? view
|
||||
: null;
|
||||
UpdateProperty(nameof(DataPackage));
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
@@ -19,6 +19,8 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
|
||||
public string Body { get; private set; } = string.Empty;
|
||||
|
||||
public ContentSize? Size { get; private set; } = ContentSize.Small;
|
||||
|
||||
// Metadata is an array of IDetailsElement,
|
||||
// where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator}
|
||||
public List<DetailsElementViewModel> Metadata { get; private set; } = [];
|
||||
@@ -40,6 +42,21 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
UpdateProperty(nameof(Body));
|
||||
UpdateProperty(nameof(HeroImage));
|
||||
|
||||
if (model is IExtendedAttributesProvider provider)
|
||||
{
|
||||
if (provider.GetProperties()?.TryGetValue("Size", out var rawValue) == true)
|
||||
{
|
||||
if (rawValue is int sizeAsInt)
|
||||
{
|
||||
Size = (ContentSize)sizeAsInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Size ??= ContentSize.Small;
|
||||
|
||||
UpdateProperty(nameof(Size));
|
||||
|
||||
var meta = model.Metadata;
|
||||
if (meta is not null)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
@@ -57,7 +58,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
// because each call to GetProperties() is a cross process hop, and if you
|
||||
// marshal-by-value the property set, then you don't want to throw it away and
|
||||
// re-marshal it for every property. MAKE SURE YOU CACHE IT.
|
||||
if (props?.TryGetValue("FontFamily", out var family) ?? false)
|
||||
if (props?.TryGetValue(WellKnownExtensionAttributes.FontFamily, out var family) ?? false)
|
||||
{
|
||||
FontFamily = family as string;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
|
||||
public string Section { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsSectionOrSeparator { get; private set; }
|
||||
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
@@ -82,14 +84,18 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
|
||||
UpdateTags(li.Tags);
|
||||
|
||||
Section = li.Section ?? string.Empty;
|
||||
|
||||
UpdateProperty(nameof(Section));
|
||||
IsSectionOrSeparator = IsSeparator(li);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
|
||||
UpdateAccessibleName();
|
||||
}
|
||||
|
||||
private bool IsSeparator(IListItem item)
|
||||
{
|
||||
return item.Command is null;
|
||||
}
|
||||
|
||||
public override void SlowInitializeProperties()
|
||||
{
|
||||
base.SlowInitializeProperties();
|
||||
@@ -104,8 +110,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
{
|
||||
Details = new(extensionDetails, PageContext);
|
||||
Details.InitializeProperties();
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
}
|
||||
|
||||
AddShowDetailsCommands();
|
||||
@@ -135,14 +140,18 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
break;
|
||||
case nameof(model.Section):
|
||||
Section = model.Section ?? string.Empty;
|
||||
UpdateProperty(nameof(Section));
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
|
||||
break;
|
||||
case nameof(model.Details):
|
||||
case nameof(model.Command):
|
||||
IsSectionOrSeparator = IsSeparator(model);
|
||||
UpdateProperty(nameof(IsSectionOrSeparator));
|
||||
break;
|
||||
case nameof(Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
Details?.InitializeProperties();
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
UpdateShowDetailsCommand();
|
||||
break;
|
||||
case nameof(model.MoreCommands):
|
||||
@@ -194,8 +203,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,8 +235,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record DismissMessage()
|
||||
{
|
||||
}
|
||||
public record DismissMessage(bool ForceGoHome = false);
|
||||
|
||||
@@ -4,6 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record NavigateBackMessage(bool FromBackspace = false)
|
||||
{
|
||||
}
|
||||
public record NavigateBackMessage(bool FromBackspace = false);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to navigate left in a grid view when pressing the Left arrow key in the SearchBox.
|
||||
/// </summary>
|
||||
public record NavigateLeftCommand;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to navigate right in a grid view when pressing the Right arrow key in the SearchBox.
|
||||
/// </summary>
|
||||
public record NavigateRightCommand;
|
||||
@@ -3,13 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class SeparatorViewModel() :
|
||||
CommandItem,
|
||||
IContextItemViewModel,
|
||||
IFilterItemViewModel,
|
||||
ISeparatorContextItem,
|
||||
|
||||
@@ -14,6 +14,7 @@ using Microsoft.CommandPalette.Extensions;
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
{
|
||||
@@ -378,7 +379,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
{
|
||||
// Reset the palette to the main page and dismiss
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
||||
WeakReferenceMessenger.Default.Send(new DismissMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -398,7 +399,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
case CommandResultKind.Hide:
|
||||
{
|
||||
// Keep this page open, but hide the palette.
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
||||
WeakReferenceMessenger.Default.Send(new DismissMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -460,4 +461,12 @@ public partial class ShellViewModel : ObservableObject,
|
||||
{
|
||||
_navigationCts?.Cancel();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_handleInvokeTask?.Dispose();
|
||||
_navigationCts?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
Color.FromArgb(255, 255, 185, 0), // #ffb900
|
||||
Color.FromArgb(255, 255, 140, 0), // #ff8c00
|
||||
Color.FromArgb(255, 247, 99, 12), // #f7630c
|
||||
Color.FromArgb(255, 202, 80, 16), // #ca5010
|
||||
Color.FromArgb(255, 218, 59, 1), // #da3b01
|
||||
Color.FromArgb(255, 239, 105, 80), // #ef6950
|
||||
|
||||
// row 1
|
||||
Color.FromArgb(255, 209, 52, 56), // #d13438
|
||||
Color.FromArgb(255, 255, 67, 67), // #ff4343
|
||||
Color.FromArgb(255, 231, 72, 86), // #e74856
|
||||
Color.FromArgb(255, 232, 17, 35), // #e81123
|
||||
Color.FromArgb(255, 234, 0, 94), // #ea005e
|
||||
Color.FromArgb(255, 195, 0, 82), // #c30052
|
||||
|
||||
// row 2
|
||||
Color.FromArgb(255, 227, 0, 140), // #e3008c
|
||||
Color.FromArgb(255, 191, 0, 119), // #bf0077
|
||||
Color.FromArgb(255, 194, 57, 179), // #c239b3
|
||||
Color.FromArgb(255, 154, 0, 137), // #9a0089
|
||||
Color.FromArgb(255, 0, 120, 212), // #0078d4
|
||||
Color.FromArgb(255, 0, 99, 177), // #0063b1
|
||||
|
||||
// row 3
|
||||
Color.FromArgb(255, 142, 140, 216), // #8e8cd8
|
||||
Color.FromArgb(255, 107, 105, 214), // #6b69d6
|
||||
Color.FromArgb(255, 135, 100, 184), // #8764b8
|
||||
Color.FromArgb(255, 116, 77, 169), // #744da9
|
||||
Color.FromArgb(255, 177, 70, 194), // #b146c2
|
||||
Color.FromArgb(255, 136, 23, 152), // #881798
|
||||
|
||||
// row 4
|
||||
Color.FromArgb(255, 0, 153, 188), // #0099bc
|
||||
Color.FromArgb(255, 45, 125, 154), // #2d7d9a
|
||||
Color.FromArgb(255, 0, 183, 195), // #00b7c3
|
||||
Color.FromArgb(255, 3, 131, 135), // #038387
|
||||
Color.FromArgb(255, 0, 178, 148), // #00b294
|
||||
Color.FromArgb(255, 1, 133, 116), // #018574
|
||||
|
||||
// row 5
|
||||
Color.FromArgb(255, 0, 204, 106), // #00cc6a
|
||||
Color.FromArgb(255, 16, 137, 62), // #10893e
|
||||
Color.FromArgb(255, 122, 117, 116), // #7a7574
|
||||
Color.FromArgb(255, 93, 90, 88), // #5d5a58
|
||||
Color.FromArgb(255, 104, 118, 138), // #68768a
|
||||
Color.FromArgb(255, 81, 92, 107), // #515c6b
|
||||
|
||||
// row 6
|
||||
Color.FromArgb(255, 86, 124, 115), // #567c73
|
||||
Color.FromArgb(255, 72, 104, 96), // #486860
|
||||
Color.FromArgb(255, 73, 130, 5), // #498205
|
||||
Color.FromArgb(255, 16, 124, 16), // #107c10
|
||||
Color.FromArgb(255, 118, 118, 118), // #767676
|
||||
Color.FromArgb(255, 76, 74, 72), // #4c4a48
|
||||
|
||||
// row 7
|
||||
Color.FromArgb(255, 105, 121, 126), // #69797e
|
||||
Color.FromArgb(255, 74, 84, 89), // #4a5459
|
||||
Color.FromArgb(255, 100, 124, 100), // #647c64
|
||||
Color.FromArgb(255, 82, 94, 84), // #525e54
|
||||
Color.FromArgb(255, 132, 117, 69), // #847545
|
||||
Color.FromArgb(255, 126, 115, 95), // #7e735f
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private ElementTheme? _elementThemeOverride;
|
||||
private Color _currentSystemAccentColor;
|
||||
|
||||
public ObservableCollection<Color> Swatches => WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_settings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _settings.Theme;
|
||||
set
|
||||
{
|
||||
if (_settings.Theme != value)
|
||||
{
|
||||
_settings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _settings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_settings.ColorizationMode != value)
|
||||
{
|
||||
_settings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_settings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _settings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_settings.CustomThemeColor != value)
|
||||
{
|
||||
_settings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
if (ColorIntensity == 0)
|
||||
{
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _settings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_settings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _settings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImagePath != value)
|
||||
{
|
||||
_settings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
{
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _settings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_settings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _settings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_settings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _settings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_settings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _settings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageFit != value)
|
||||
{
|
||||
_settings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageFitIndex
|
||||
{
|
||||
// Naming between UI facing string and enum is a bit confusing, but the enum fields
|
||||
// are based on XAML Stretch enum values. So I'm choosing to keep the confusion here, close
|
||||
// to the UI.
|
||||
// - BackgroundImageFit.Fill corresponds to "Stretch"
|
||||
// - BackgroundImageFit.UniformToFill corresponds to "Fill"
|
||||
get => BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => 1,
|
||||
_ => 0,
|
||||
};
|
||||
set => BackgroundImageFit = value switch
|
||||
{
|
||||
1 => BackgroundImageFit.Fill,
|
||||
_ => BackgroundImageFit.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
|
||||
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
|
||||
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
UpdateAccentColor(_uiSettings);
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
|
||||
private void UpdateAccentColor(UISettings sender)
|
||||
{
|
||||
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
|
||||
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Reapply()
|
||||
{
|
||||
// Theme services recalculates effective color and opacity based on current settings.
|
||||
EffectiveBackdrop = _themeService.Current.BackdropParameters;
|
||||
OnPropertyChanged(nameof(EffectiveBackdrop));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
|
||||
|
||||
// LOAD BEARING:
|
||||
// We need to cycle through the EffectiveTheme property to force reload of resources.
|
||||
_elementThemeOverride = ElementTheme.Light;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = ElementTheme.Dark;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = null;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetBackgroundImageProperties()
|
||||
{
|
||||
BackgroundImageBrightness = 0;
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public enum BackgroundImageFit
|
||||
{
|
||||
Fill,
|
||||
UniformToFill,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public enum ColorizationMode
|
||||
{
|
||||
None,
|
||||
WindowsAccentColor,
|
||||
CustomColor,
|
||||
Image,
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -44,6 +45,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
private List<Scored<IListItem>>? _filteredItems;
|
||||
private List<Scored<IListItem>>? _filteredApps;
|
||||
private List<Scored<IListItem>>? _fallbackItems;
|
||||
|
||||
// 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 bool _includeApps;
|
||||
private bool _filteredItemsIncludesApps;
|
||||
@@ -155,42 +159,18 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchText))
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
return _tlcManager
|
||||
.TopLevelCommands
|
||||
.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
var limitedApps = new List<Scored<IListItem>>();
|
||||
|
||||
// Fuzzy matching can produce a lot of results, so we want to limit the
|
||||
// number of apps we show at once if it's a large set.
|
||||
if (_filteredApps?.Count > 0)
|
||||
{
|
||||
limitedApps = _filteredApps.OrderByDescending(s => s.Score).Take(_appResultLimit).ToList();
|
||||
}
|
||||
|
||||
var items = Enumerable.Empty<Scored<IListItem>>()
|
||||
.Concat(_filteredItems is not null ? _filteredItems : [])
|
||||
.Concat(_scoredFallbackItems is not null ? _scoredFallbackItems : [])
|
||||
.Concat(limitedApps)
|
||||
.OrderByDescending(o => o.Score)
|
||||
|
||||
// Add fallback items post-sort so they are always at the end of the list
|
||||
// and eventually ordered based on user preference
|
||||
.Concat(_fallbackItems is not null ? _fallbackItems.Where(w => !string.IsNullOrEmpty(w.Item.Title)) : [])
|
||||
.Select(s => s.Item)
|
||||
.ToArray();
|
||||
return items;
|
||||
}
|
||||
// Either return the top-level commands (no search text), or the merged and
|
||||
// filtered results.
|
||||
return string.IsNullOrEmpty(SearchText)
|
||||
? _tlcManager.TopLevelCommands.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title)).ToArray()
|
||||
: MainListPageResultFactory.Create(
|
||||
_filteredItems,
|
||||
_scoredFallbackItems?.ToList(),
|
||||
_filteredApps,
|
||||
_fallbackItems,
|
||||
_appResultLimit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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.
|
||||
|
||||
#pragma warning disable IDE0007 // Use implicit type
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
|
||||
internal static class MainListPageResultFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a merged and ordered array of results from multiple scored input lists,
|
||||
/// 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,
|
||||
int appResultLimit)
|
||||
{
|
||||
if (appResultLimit < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(appResultLimit), "App result limit must be non-negative.");
|
||||
}
|
||||
|
||||
int len1 = filteredItems?.Count ?? 0;
|
||||
int len2 = scoredFallbackItems?.Count ?? 0;
|
||||
|
||||
// Apps are pre-sorted, so we just need to take the top N, limited by appResultLimit.
|
||||
int len3 = Math.Min(filteredApps?.Count ?? 0, appResultLimit);
|
||||
|
||||
// Allocate the exact size of the result array.
|
||||
int totalCount = len1 + len2 + len3 + GetNonEmptyFallbackItemsCount(fallbackItems);
|
||||
var result = new IListItem[totalCount];
|
||||
|
||||
// Three-way stable merge of already-sorted lists.
|
||||
int idx1 = 0, idx2 = 0, idx3 = 0;
|
||||
int writePos = 0;
|
||||
|
||||
// Merge while all three lists have items. To maintain a stable sort, the
|
||||
// priority is: list1 > list2 > list3 when scores are equal.
|
||||
while (idx1 < len1 && idx2 < len2 && idx3 < len3)
|
||||
{
|
||||
// Using null-forgiving operator as we have already checked against lengths.
|
||||
int score1 = filteredItems![idx1].Score;
|
||||
int score2 = scoredFallbackItems![idx2].Score;
|
||||
int score3 = filteredApps![idx3].Score;
|
||||
|
||||
if (score1 >= score2 && score1 >= score3)
|
||||
{
|
||||
result[writePos++] = filteredItems[idx1++].Item;
|
||||
}
|
||||
else if (score2 >= score3)
|
||||
{
|
||||
result[writePos++] = scoredFallbackItems[idx2++].Item;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[writePos++] = filteredApps[idx3++].Item;
|
||||
}
|
||||
}
|
||||
|
||||
// Two-way merges for remaining pairs.
|
||||
while (idx1 < len1 && idx2 < len2)
|
||||
{
|
||||
if (filteredItems![idx1].Score >= scoredFallbackItems![idx2].Score)
|
||||
{
|
||||
result[writePos++] = filteredItems[idx1++].Item;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[writePos++] = scoredFallbackItems[idx2++].Item;
|
||||
}
|
||||
}
|
||||
|
||||
while (idx1 < len1 && idx3 < len3)
|
||||
{
|
||||
if (filteredItems![idx1].Score >= filteredApps![idx3].Score)
|
||||
{
|
||||
result[writePos++] = filteredItems[idx1++].Item;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[writePos++] = filteredApps[idx3++].Item;
|
||||
}
|
||||
}
|
||||
|
||||
while (idx2 < len2 && idx3 < len3)
|
||||
{
|
||||
if (scoredFallbackItems![idx2].Score >= filteredApps![idx3].Score)
|
||||
{
|
||||
result[writePos++] = scoredFallbackItems[idx2++].Item;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[writePos++] = filteredApps[idx3++].Item;
|
||||
}
|
||||
}
|
||||
|
||||
// Drain remaining items from a non-empty list.
|
||||
while (idx1 < len1)
|
||||
{
|
||||
result[writePos++] = filteredItems![idx1++].Item;
|
||||
}
|
||||
|
||||
while (idx2 < len2)
|
||||
{
|
||||
result[writePos++] = scoredFallbackItems![idx2++].Item;
|
||||
}
|
||||
|
||||
while (idx3 < len3)
|
||||
{
|
||||
result[writePos++] = filteredApps![idx3++].Item;
|
||||
}
|
||||
|
||||
// Append filtered fallback items. Fallback items are added post-sort so they are
|
||||
// always at the end of the list and eventually ordered based on user preference.
|
||||
if (fallbackItems is not null)
|
||||
{
|
||||
for (int i = 0; i < fallbackItems.Count; i++)
|
||||
{
|
||||
var item = fallbackItems[i].Item;
|
||||
if (!string.IsNullOrEmpty(item.Title))
|
||||
{
|
||||
result[writePos++] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int GetNonEmptyFallbackItemsCount(IList<Scored<IListItem>>? fallbackItems)
|
||||
{
|
||||
int fallbackItemsCount = 0;
|
||||
|
||||
if (fallbackItems is not null)
|
||||
{
|
||||
for (int i = 0; i < fallbackItems.Count; i++)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fallbackItems[i].Item.Title))
|
||||
{
|
||||
fallbackItemsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackItemsCount;
|
||||
}
|
||||
}
|
||||
#pragma warning restore IDE0007 // Use implicit type
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ImageSource? BackgroundImageSource { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageOpacity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundImageTint { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageTintIntensity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int BackgroundImageBlurAmount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageBrightness { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowBackgroundImage { get; private set; }
|
||||
|
||||
public MainWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
BackgroundImageSource = _themeService.Current.BackgroundImageSource;
|
||||
BackgroundImageStretch = _themeService.Current.BackgroundImageStretch;
|
||||
BackgroundImageOpacity = _themeService.Current.BackgroundImageOpacity;
|
||||
|
||||
BackgroundImageBrightness = _themeService.Current.BackgroundBrightness;
|
||||
BackgroundImageTint = _themeService.Current.Tint;
|
||||
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
|
||||
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,12 @@
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="AdaptiveCards.Templating" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="Microsoft.Bot.AdaptiveExpressions.Core" />
|
||||
<PackageReference Include="AdaptiveCards.ObjectModel.WinUI3" GeneratePathProperty="true">
|
||||
<ExcludeAssets>compile</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True" >
|
||||
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True">
|
||||
<ExcludeAssets>compile</ExcludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -411,6 +411,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pick background image.
|
||||
/// </summary>
|
||||
public static string builtin_settings_appearance_pick_background_image_title {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_settings_appearance_pick_background_image_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} extensions found.
|
||||
/// </summary>
|
||||
|
||||
@@ -239,4 +239,7 @@
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides theme-related values for the Command Palette and notifies listeners about
|
||||
/// changes that affect visual appearance (theme, tint, background image, and backdrop).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations are expected to monitor system/app theme changes and raise
|
||||
/// <see cref="ThemeChanged"/> accordingly. Consumers should call <see cref="Initialize"/>
|
||||
/// once to hook required sources and then query properties/methods for the current visuals.
|
||||
/// </remarks>
|
||||
public interface IThemeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the effective theme or any visual-affecting setting changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Triggered for changes such as app theme (light/dark/default), background image,
|
||||
/// tint/accent, or backdrop parameters that would require UI to refresh styling.
|
||||
/// </remarks>
|
||||
event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the theme service and starts listening for theme-related changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Safe to call once during application startup before consuming the service.
|
||||
/// </remarks>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current theme settings.
|
||||
/// </summary>
|
||||
ThemeSnapshot Current { get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for theme-related changes. </summary>
|
||||
public class ThemeChangedEventArgs : EventArgs;
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of theme-related visual settings, including accent color, theme preference, and background
|
||||
/// image configuration, for use in rendering the Command Palette UI.
|
||||
/// </summary>
|
||||
public sealed class ThemeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Command Palette visuals.
|
||||
/// </summary>
|
||||
public required Color Tint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Command Palette visuals.
|
||||
/// </summary>
|
||||
public required float TintIntensity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured application theme preference.
|
||||
/// </summary>
|
||||
public required ElementTheme Theme { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image source to render as the background, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="null"/> when no background image is configured.
|
||||
/// </remarks>
|
||||
public required ImageSource? BackgroundImageSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stretch mode used to lay out the background image.
|
||||
/// </summary>
|
||||
public required Stretch BackgroundImageStretch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opacity applied to the background image.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
|
||||
/// </value>
|
||||
public required double BackgroundImageOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
||||
/// </summary>
|
||||
/// <returns>The resolved <c>AcrylicBackdropParameters</c> to apply.</returns>
|
||||
public required AcrylicBackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
}
|
||||
@@ -11,7 +11,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -60,6 +62,26 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -282,3 +304,11 @@ public enum MonitorBehavior
|
||||
InPlace = 3,
|
||||
ToLast = 4,
|
||||
}
|
||||
|
||||
public enum EscapeKeyBehavior
|
||||
{
|
||||
ClearSearchFirstThenGoBack = 0,
|
||||
AlwaysGoBack = 1,
|
||||
AlwaysDismiss = 2,
|
||||
AlwaysHide = 3,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -29,6 +31,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public AppearanceSettingsViewModel Appearance { get; }
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
@@ -160,6 +164,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public int EscapeKeyBehaviorIndex
|
||||
{
|
||||
get => (int)_settings.EscapeKeyBehaviorSetting;
|
||||
set
|
||||
{
|
||||
_settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
@@ -169,6 +183,9 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
_settings = settings;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
var themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
@@ -232,6 +232,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
UpdateInitialIcon();
|
||||
}
|
||||
else if (e.PropertyName == nameof(CommandItem.DataPackage))
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(CommandItem.DataPackage));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,4 +401,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}";
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetProperties()
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public enum UserTheme
|
||||
{
|
||||
Default,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
@@ -4,19 +4,23 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI">
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:services="using:Microsoft.CmdPal.UI.Services">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="Controls/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
|
||||
<!-- Default theme dictionary -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
|
||||
<services:MutableOverridesDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker;
|
||||
using Microsoft.CmdPal.Ext.WinGet;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
@@ -112,6 +114,17 @@ public partial class App : Application
|
||||
// Root services
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
AddBuiltInCommands(services);
|
||||
|
||||
AddCoreServices(services);
|
||||
|
||||
AddUIServices(services);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void AddBuiltInCommands(ServiceCollection services)
|
||||
{
|
||||
// Built-in Commands. Order matters - this is the order they'll be presented by default.
|
||||
var allApps = new AllAppsCommandProvider();
|
||||
var files = new IndexerCommandsProvider();
|
||||
@@ -154,17 +167,32 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
{
|
||||
// Models
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
services.AddSingleton<AliasManager>();
|
||||
services.AddSingleton<HotkeyManager>();
|
||||
var sm = SettingsModel.LoadSettings();
|
||||
services.AddSingleton(sm);
|
||||
var state = AppStateModel.LoadState();
|
||||
services.AddSingleton(state);
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
|
||||
// Services
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
services.AddSingleton<AliasManager>();
|
||||
services.AddSingleton<HotkeyManager>();
|
||||
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<TrayIconService>();
|
||||
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<ResourceSwapper>();
|
||||
}
|
||||
|
||||
private static void AddCoreServices(ServiceCollection services)
|
||||
{
|
||||
// Core services
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
services.AddSingleton<IRunHistoryService, RunHistoryService>();
|
||||
|
||||
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
|
||||
@@ -174,7 +202,5 @@ public partial class App : Application
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
internal sealed partial class BlurImageControl : Control
|
||||
{
|
||||
private const string ImageSourceParameterName = "ImageSource";
|
||||
|
||||
private const string BrightnessEffectName = "Brightness";
|
||||
private const string BrightnessOverlayEffectName = "BrightnessOverlay";
|
||||
private const string BlurEffectName = "Blur";
|
||||
private const string TintBlendEffectName = "TintBlend";
|
||||
private const string TintEffectName = "Tint";
|
||||
|
||||
#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
|
||||
private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
|
||||
private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
|
||||
private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
|
||||
private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
|
||||
private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
|
||||
#pragma warning restore CA1507
|
||||
|
||||
private static readonly string[] AnimatableProperties = [
|
||||
BrightnessSource1AmountEffectProperty,
|
||||
BrightnessSource2AmountEffectProperty,
|
||||
BrightnessOverlayColorEffectProperty,
|
||||
BlurBlurAmountEffectProperty,
|
||||
TintColorEffectProperty
|
||||
];
|
||||
|
||||
public static readonly DependencyProperty ImageSourceProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageSource),
|
||||
typeof(ImageSource),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(null, OnImageChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageStretchProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageStretch),
|
||||
typeof(Stretch),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageOpacityProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageOpacity),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(1.0, OnOpacityChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageBrightnessProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ImageBrightness),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(1.0, OnBrightnessChanged));
|
||||
|
||||
public static readonly DependencyProperty BlurAmountProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(BlurAmount),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(0.0, OnBlurAmountChanged));
|
||||
|
||||
public static readonly DependencyProperty TintColorProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(TintColor),
|
||||
typeof(Color),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));
|
||||
|
||||
public static readonly DependencyProperty TintIntensityProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(TintIntensity),
|
||||
typeof(double),
|
||||
typeof(BlurImageControl),
|
||||
new PropertyMetadata(0.0, OnVisualPropertyChanged));
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual? _effectVisual;
|
||||
private CompositionEffectBrush? _effectBrush;
|
||||
private CompositionSurfaceBrush? _imageBrush;
|
||||
|
||||
public BlurImageControl()
|
||||
{
|
||||
this.DefaultStyleKey = typeof(BlurImageControl);
|
||||
this.Loaded += OnLoaded;
|
||||
this.SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
public ImageSource ImageSource
|
||||
{
|
||||
get => (ImageSource)GetValue(ImageSourceProperty);
|
||||
set => SetValue(ImageSourceProperty, value);
|
||||
}
|
||||
|
||||
public Stretch ImageStretch
|
||||
{
|
||||
get => (Stretch)GetValue(ImageStretchProperty);
|
||||
set => SetValue(ImageStretchProperty, value);
|
||||
}
|
||||
|
||||
public double ImageOpacity
|
||||
{
|
||||
get => (double)GetValue(ImageOpacityProperty);
|
||||
set => SetValue(ImageOpacityProperty, value);
|
||||
}
|
||||
|
||||
public double ImageBrightness
|
||||
{
|
||||
get => (double)GetValue(ImageBrightnessProperty);
|
||||
set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
|
||||
}
|
||||
|
||||
public double BlurAmount
|
||||
{
|
||||
get => (double)GetValue(BlurAmountProperty);
|
||||
set => SetValue(BlurAmountProperty, value);
|
||||
}
|
||||
|
||||
public Color TintColor
|
||||
{
|
||||
get => (Color)GetValue(TintColorProperty);
|
||||
set => SetValue(TintColorProperty, value);
|
||||
}
|
||||
|
||||
public double TintIntensity
|
||||
{
|
||||
get => (double)GetValue(TintIntensityProperty);
|
||||
set => SetValue(TintIntensityProperty, value);
|
||||
}
|
||||
|
||||
private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._imageBrush != null)
|
||||
{
|
||||
control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._compositor != null)
|
||||
{
|
||||
control.UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._effectVisual != null)
|
||||
{
|
||||
control._effectVisual.Opacity = (float)(double)e.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._effectBrush != null)
|
||||
{
|
||||
control.UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is BlurImageControl control && control._effectBrush != null)
|
||||
{
|
||||
control.UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
InitializeComposition();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (_effectVisual != null)
|
||||
{
|
||||
_effectVisual.Size = new Vector2(
|
||||
(float)Math.Max(1, e.NewSize.Width),
|
||||
(float)Math.Max(1, e.NewSize.Height));
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not BlurImageControl control)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
control.EnsureEffect(force: true);
|
||||
control.UpdateEffect();
|
||||
}
|
||||
|
||||
private void InitializeComposition()
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(this);
|
||||
_compositor = visual.Compositor;
|
||||
|
||||
_effectVisual = _compositor.CreateSpriteVisual();
|
||||
_effectVisual.Size = new Vector2(
|
||||
(float)Math.Max(1, ActualWidth),
|
||||
(float)Math.Max(1, ActualHeight));
|
||||
_effectVisual.Opacity = (float)ImageOpacity;
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);
|
||||
|
||||
UpdateEffect();
|
||||
}
|
||||
|
||||
private void EnsureEffect(bool force = false)
|
||||
{
|
||||
if (_compositor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_effectBrush is not null && !force)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);
|
||||
|
||||
// 1) Brightness via ArithmeticCompositeEffect
|
||||
// We blend between the original image and either black or white,
|
||||
// depending on whether we want to darken or brighten. BrightnessEffect isn't supported
|
||||
// in the composition graph.
|
||||
var brightnessEffect = new ArithmeticCompositeEffect
|
||||
{
|
||||
Name = BrightnessEffectName,
|
||||
Source1 = imageSource, // original image
|
||||
Source2 = new ColorSourceEffect
|
||||
{
|
||||
Name = BrightnessOverlayEffectName,
|
||||
Color = Colors.Black, // we'll swap black/white via properties
|
||||
},
|
||||
|
||||
MultiplyAmount = 0.0f,
|
||||
Source1Amount = 1.0f, // original
|
||||
Source2Amount = 0.0f, // overlay
|
||||
Offset = 0.0f,
|
||||
};
|
||||
|
||||
// 2) Blur
|
||||
var blurEffect = new GaussianBlurEffect
|
||||
{
|
||||
Name = BlurEffectName,
|
||||
BlurAmount = 0.0f,
|
||||
BorderMode = EffectBorderMode.Hard,
|
||||
Optimization = EffectOptimization.Balanced,
|
||||
Source = brightnessEffect,
|
||||
};
|
||||
|
||||
// 3) Tint (always in the chain; intensity via alpha)
|
||||
var tintEffect = new BlendEffect
|
||||
{
|
||||
Name = TintBlendEffectName,
|
||||
Background = blurEffect,
|
||||
Foreground = new ColorSourceEffect
|
||||
{
|
||||
Name = TintEffectName,
|
||||
Color = Colors.Transparent,
|
||||
},
|
||||
Mode = BlendEffectMode.Multiply,
|
||||
};
|
||||
|
||||
var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);
|
||||
|
||||
_effectBrush?.Dispose();
|
||||
_effectBrush = effectFactory.CreateBrush();
|
||||
|
||||
// Set initial source
|
||||
if (ImageSource is not null)
|
||||
{
|
||||
_imageBrush ??= _compositor.CreateSurfaceBrush();
|
||||
LoadImageAsync(ImageSource);
|
||||
_effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
}
|
||||
else
|
||||
{
|
||||
_effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
|
||||
}
|
||||
|
||||
if (_effectVisual is not null)
|
||||
{
|
||||
_effectVisual.Brush = _effectBrush;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEffect()
|
||||
{
|
||||
if (_compositor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureEffect();
|
||||
if (_effectBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var props = _effectBrush.Properties;
|
||||
|
||||
// Brightness
|
||||
var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);
|
||||
|
||||
float source1Amount;
|
||||
float source2Amount;
|
||||
Color overlayColor;
|
||||
|
||||
if (b >= 0)
|
||||
{
|
||||
// Brighten: blend towards white
|
||||
overlayColor = Colors.White;
|
||||
source1Amount = 1.0f - b; // original image contribution
|
||||
source2Amount = b; // white overlay contribution
|
||||
}
|
||||
else
|
||||
{
|
||||
// Darken: blend towards black
|
||||
overlayColor = Colors.Black;
|
||||
var t = -b; // 0..1
|
||||
source1Amount = 1.0f - t; // original image
|
||||
source2Amount = t; // black overlay
|
||||
}
|
||||
|
||||
props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
|
||||
props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
|
||||
props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);
|
||||
|
||||
// Blur
|
||||
props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);
|
||||
|
||||
// Tint
|
||||
var tintColor = TintColor;
|
||||
var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);
|
||||
|
||||
var adjustedColor = Color.FromArgb(
|
||||
(byte)(clampedIntensity * 255),
|
||||
tintColor.R,
|
||||
tintColor.G,
|
||||
tintColor.B);
|
||||
|
||||
props.InsertColor(TintColorEffectProperty, adjustedColor);
|
||||
}
|
||||
|
||||
private void LoadImageAsync(ImageSource imageSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_imageBrush ??= _compositor?.CreateSurfaceBrush();
|
||||
if (_imageBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
|
||||
{
|
||||
switch (e.Status)
|
||||
{
|
||||
case LoadedImageSourceLoadStatus.Success:
|
||||
Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");
|
||||
|
||||
try
|
||||
{
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to set surface in BlurImageControl", ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
break;
|
||||
case LoadedImageSourceLoadStatus.NetworkError:
|
||||
case LoadedImageSourceLoadStatus.InvalidFormat:
|
||||
case LoadedImageSourceLoadStatus.Other:
|
||||
default:
|
||||
Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
|
||||
{
|
||||
var surfaceBrush = _imageBrush;
|
||||
if (surfaceBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
surfaceBrush.Surface = loadedSurface;
|
||||
surfaceBrush.Stretch = ConvertStretch(ImageStretch);
|
||||
surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
|
||||
}
|
||||
|
||||
private static CompositionStretch ConvertStretch(Stretch stretch)
|
||||
{
|
||||
return stretch switch
|
||||
{
|
||||
Stretch.None => CompositionStretch.None,
|
||||
Stretch.Fill => CompositionStretch.Fill,
|
||||
Stretch.Uniform => CompositionStretch.Uniform,
|
||||
Stretch.UniformToFill => CompositionStretch.UniformToFill,
|
||||
_ => CompositionStretch.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ColorPalette"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:localConverters="using:Microsoft.CmdPal.UI.Converters"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:toolkitConverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
|
||||
<toolkitConverters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
|
||||
<localConverters:ContrastBrushConverter x:Key="ContrastBrushConverter" />
|
||||
|
||||
<Style x:Key="PaletteGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
|
||||
<Setter Property="TabNavigation" Value="Local" />
|
||||
<Setter Property="IsHoldingEnabled" Value="True" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MinWidth" Value="32" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-2" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
<Grid
|
||||
x:Name="ContentBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform x:Name="ContentBorderScale" />
|
||||
</Grid.RenderTransform>
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}" />
|
||||
<!--
|
||||
The 'Xg' text simulates the amount of space one line of text will occupy.
|
||||
In the DataPlaceholder state, the Content is not loaded yet so we
|
||||
approximate the size of the item using placeholder text.
|
||||
-->
|
||||
<TextBlock
|
||||
x:Name="PlaceholderTextBlock"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{x:Null}"
|
||||
IsHitTestVisible="False"
|
||||
Text="Xg"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle
|
||||
x:Name="PlaceholderRect"
|
||||
Fill="{ThemeResource ListViewItemPlaceholderBackground}"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle
|
||||
x:Name="BorderRectangle"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
RadiusX="6"
|
||||
RadiusY="6"
|
||||
Stroke="{ThemeResource SystemControlHighlightListAccentLowBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Border
|
||||
x:Name="MultiSelectSquare"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="0,2,2,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource SystemControlBackgroundChromeMediumBrush}"
|
||||
CornerRadius="6"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="MultiSelectCheck"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemControlForegroundBaseMediumHighBrush}"
|
||||
Glyph=""
|
||||
Opacity="0" />
|
||||
</Border>
|
||||
<Border
|
||||
x:Name="MultiArrangeOverlayTextBorder"
|
||||
Height="20"
|
||||
MinWidth="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SystemControlBackgroundAccentBrush}"
|
||||
BorderBrush="{ThemeResource SystemControlBackgroundChromeWhiteBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="6"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0">
|
||||
<TextBlock
|
||||
x:Name="MultiArrangeOverlayText"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.DragItemsCount}" />
|
||||
</Border>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="FocusStates">
|
||||
<VisualState x:Name="Focused">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Unfocused" />
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal">
|
||||
|
||||
<Storyboard>
|
||||
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
|
||||
<VisualState x:Name="PointerOver">
|
||||
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="BorderRectangle"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0" />
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Stroke">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryThickness">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="2" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="MultiSelectCheck"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0" />
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="MultiSelectSquare" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<GridView
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListViewBase_OnItemClick"
|
||||
ItemContainerStyle="{StaticResource PaletteGridViewItemStyle}"
|
||||
ItemsSource="{x:Bind PaletteColors}"
|
||||
SelectionMode="None">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:UniformGrid ui:FrameworkElementExtensions.AncestorType="local:ColorPalette" Columns="{Binding (ui:FrameworkElementExtensions.Ancestor).CustomPaletteColumnCount, RelativeSource={RelativeSource Self}}" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate x:DataType="Color">
|
||||
<Border
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
|
||||
CornerRadius="4"
|
||||
ToolTipService.ToolTip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ColorPalette : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection<Color>), typeof(ColorPalette), null!)!;
|
||||
|
||||
public static readonly DependencyProperty CustomPaletteColumnCountProperty = DependencyProperty.Register(nameof(CustomPaletteColumnCount), typeof(int), typeof(ColorPalette), new PropertyMetadata(10))!;
|
||||
|
||||
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPalette), new PropertyMetadata(null!))!;
|
||||
|
||||
public event EventHandler<Color?>? SelectedColorChanged;
|
||||
|
||||
private Color? _selectedColor;
|
||||
|
||||
public Color? SelectedColor
|
||||
{
|
||||
get => _selectedColor;
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedColor != value)
|
||||
{
|
||||
_selectedColor = value;
|
||||
if (value is not null)
|
||||
{
|
||||
SetValue(SelectedColorProperty, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearValue(SelectedColorProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<Color> PaletteColors
|
||||
{
|
||||
get => (ObservableCollection<Color>)GetValue(PaletteColorsProperty)!;
|
||||
set => SetValue(PaletteColorsProperty, value);
|
||||
}
|
||||
|
||||
public int CustomPaletteColumnCount
|
||||
{
|
||||
get => (int)GetValue(CustomPaletteColumnCountProperty);
|
||||
set => SetValue(CustomPaletteColumnCountProperty, value);
|
||||
}
|
||||
|
||||
public ColorPalette()
|
||||
{
|
||||
PaletteColors = [];
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ListViewBase_OnItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is Color color)
|
||||
{
|
||||
SelectedColor = color;
|
||||
SelectedColorChanged?.Invoke(this, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ColorPickerButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:BoolToVisibilityConverter
|
||||
x:Key="BoolToVisibilityInvertedConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<DropDownButton Padding="{x:Bind ToDropDownPadding(HasSelectedColor), Mode=OneWay}">
|
||||
<Grid>
|
||||
<TextBlock x:Uid="OptionalColorPickerButton_UnsetTextBlock" Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityInvertedConverter}}" />
|
||||
<Border
|
||||
x:Name="ColorPreviewBorder"
|
||||
Width="48"
|
||||
Height="24"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind SelectedColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Grid>
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout
|
||||
x:Name="ColorPickerFlyout"
|
||||
Opened="FlyoutBase_OnOpened"
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<Flyout.FlyoutPresenterStyle>
|
||||
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
|
||||
<Setter Property="MinWidth" Value="660" />
|
||||
</Style>
|
||||
</Flyout.FlyoutPresenterStyle>
|
||||
<StackPanel
|
||||
x:Name="FlyoutRoot"
|
||||
Orientation="Horizontal"
|
||||
SizeChanged="FlyoutRoot_OnSizeChanged"
|
||||
Spacing="20">
|
||||
<!-- Left column: Preset colors and reset button -->
|
||||
<StackPanel Margin="2">
|
||||
<TextBlock
|
||||
x:Uid="OptionalColorPickerButton_WindowsColorsSectionHeading"
|
||||
Margin="0,0,0,12"
|
||||
Style="{StaticResource BodyTextBlockStyle}" />
|
||||
|
||||
<controls:ColorPalette
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
CustomPaletteColumnCount="9"
|
||||
PaletteColors="{x:Bind PaletteColors}"
|
||||
SelectedColorChanged="ColorPalette_OnSelectedColorChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right column: Spectrum -->
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Uid="OptionalColorPickerButton_CustomColorsSectionHeading"
|
||||
Margin="0,0,0,12"
|
||||
Style="{StaticResource BodyTextBlockStyle}" />
|
||||
|
||||
<ColorPicker
|
||||
IsAlphaEnabled="{x:Bind IsAlphaEnabled, Mode=OneWay}"
|
||||
IsAlphaSliderVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
|
||||
IsAlphaTextInputVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
|
||||
IsColorChannelTextInputVisible="True"
|
||||
IsColorSliderVisible="True"
|
||||
IsHexInputVisible="True"
|
||||
IsMoreButtonVisible="True"
|
||||
Color="{x:Bind SelectedColor, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ColorPickerButton : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection<Color>), typeof(ColorPickerButton), new PropertyMetadata(new ObservableCollection<Color>()))!;
|
||||
|
||||
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerButton), new PropertyMetadata(Colors.Black))!;
|
||||
|
||||
public static readonly DependencyProperty IsAlphaEnabledProperty = DependencyProperty.Register(nameof(IsAlphaEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(defaultValue: false))!;
|
||||
|
||||
public static readonly DependencyProperty IsValueEditorEnabledProperty = DependencyProperty.Register(nameof(IsValueEditorEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
|
||||
|
||||
public static readonly DependencyProperty HasSelectedColorProperty = DependencyProperty.Register(nameof(HasSelectedColor), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
|
||||
|
||||
private Color _selectedColor;
|
||||
|
||||
public Color SelectedColor
|
||||
{
|
||||
get
|
||||
{
|
||||
return _selectedColor;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedColor != value)
|
||||
{
|
||||
_selectedColor = value;
|
||||
SetValue(SelectedColorProperty, value);
|
||||
HasSelectedColor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSelectedColor
|
||||
{
|
||||
get { return (bool)GetValue(HasSelectedColorProperty); }
|
||||
set { SetValue(HasSelectedColorProperty, value); }
|
||||
}
|
||||
|
||||
public bool IsAlphaEnabled
|
||||
{
|
||||
get => (bool)GetValue(IsAlphaEnabledProperty);
|
||||
set => SetValue(IsAlphaEnabledProperty, value);
|
||||
}
|
||||
|
||||
public bool IsValueEditorEnabled
|
||||
{
|
||||
get { return (bool)GetValue(IsValueEditorEnabledProperty); }
|
||||
set { SetValue(IsValueEditorEnabledProperty, value); }
|
||||
}
|
||||
|
||||
public ObservableCollection<Color> PaletteColors
|
||||
{
|
||||
get { return (ObservableCollection<Color>)GetValue(PaletteColorsProperty); }
|
||||
set { SetValue(PaletteColorsProperty, value); }
|
||||
}
|
||||
|
||||
public ColorPickerButton()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
IsEnabledChanged -= ColorPickerButton_IsEnabledChanged;
|
||||
SetEnabledState();
|
||||
IsEnabledChanged += ColorPickerButton_IsEnabledChanged;
|
||||
}
|
||||
|
||||
private void ColorPickerButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
if (this.IsEnabled)
|
||||
{
|
||||
ColorPreviewBorder.Opacity = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ColorPreviewBorder.Opacity = 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
private void ColorPalette_OnSelectedColorChanged(object? sender, Color? e)
|
||||
{
|
||||
if (e.HasValue)
|
||||
{
|
||||
HasSelectedColor = true;
|
||||
SelectedColor = e.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void FlyoutBase_OnOpened(object? sender, object e)
|
||||
{
|
||||
if (sender is not Flyout flyout || (flyout.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FlyoutRoot!.UpdateLayout();
|
||||
flyoutPresenter.UpdateLayout();
|
||||
|
||||
// Logger.LogInfo($"FlyoutBase_OnOpened: {flyoutPresenter}, {FlyoutRoot!.ActualWidth}");
|
||||
flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
|
||||
flyoutPresenter.MinWidth = 660;
|
||||
flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
|
||||
}
|
||||
|
||||
private void FlyoutRoot_OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if ((ColorPickerFlyout!.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FlyoutRoot!.UpdateLayout();
|
||||
flyoutPresenter.UpdateLayout();
|
||||
|
||||
flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
|
||||
flyoutPresenter.MinWidth = 660;
|
||||
flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
|
||||
}
|
||||
|
||||
private Thickness ToDropDownPadding(bool hasColor)
|
||||
{
|
||||
return hasColor ? new Thickness(3, 3, 8, 3) : new Thickness(8, 4, 8, 4);
|
||||
}
|
||||
|
||||
private void ResetButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HasSelectedColor = false;
|
||||
ColorPickerFlyout?.Hide();
|
||||
}
|
||||
}
|
||||
@@ -142,9 +142,9 @@
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
x:Uid="SettingsButtonTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="Settings" />
|
||||
Style="{StaticResource CaptionTextBlockStyle}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.CommandPalettePreview"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<Border
|
||||
Width="200"
|
||||
Height="120"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Translation="0,0,8">
|
||||
<Grid>
|
||||
<Border
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<AcrylicBrush
|
||||
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
<local:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
TintColor="{x:Bind PreviewBackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind ToTintIntensity(PreviewBackgroundImageTintIntensity), Mode=OneWay}"
|
||||
Visibility="{x:Bind ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="100" />
|
||||
<RowDefinition Height="20" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
x:Name="ContentPreview"
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}" BorderThickness="0,0,0,1" />
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
x:Name="CommandBarPreview"
|
||||
Grid.Row="1"
|
||||
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
|
||||
BorderBrush="{ThemeResource CmdPal.CommandBarBorderBrush}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class CommandPalettePreview : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageBrightnessProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBrightness), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageBlurAmountProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBlurAmount), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageTintProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTint), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
||||
|
||||
public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
public BackgroundImageFit PreviewBackgroundImageFit
|
||||
{
|
||||
get { return (BackgroundImageFit)GetValue(PreviewBackgroundImageFitProperty); }
|
||||
set { SetValue(PreviewBackgroundImageFitProperty, value); }
|
||||
}
|
||||
|
||||
public double PreviewBackgroundOpacity
|
||||
{
|
||||
get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
|
||||
set { SetValue(PreviewBackgroundOpacityProperty, value); }
|
||||
}
|
||||
|
||||
public Color PreviewBackgroundColor
|
||||
{
|
||||
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
|
||||
set { SetValue(PreviewBackgroundColorProperty, value); }
|
||||
}
|
||||
|
||||
public ImageSource PreviewBackgroundImageSource
|
||||
{
|
||||
get { return (ImageSource)GetValue(PreviewBackgroundImageSourceProperty); }
|
||||
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
|
||||
}
|
||||
|
||||
public int PreviewBackgroundImageOpacity
|
||||
{
|
||||
get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
|
||||
set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
|
||||
}
|
||||
|
||||
public double PreviewBackgroundImageBrightness
|
||||
{
|
||||
get => (double)GetValue(PreviewBackgroundImageBrightnessProperty);
|
||||
set => SetValue(PreviewBackgroundImageBrightnessProperty, value);
|
||||
}
|
||||
|
||||
public double PreviewBackgroundImageBlurAmount
|
||||
{
|
||||
get => (double)GetValue(PreviewBackgroundImageBlurAmountProperty);
|
||||
set => SetValue(PreviewBackgroundImageBlurAmountProperty, value);
|
||||
}
|
||||
|
||||
public Color PreviewBackgroundImageTint
|
||||
{
|
||||
get => (Color)GetValue(PreviewBackgroundImageTintProperty);
|
||||
set => SetValue(PreviewBackgroundImageTintProperty, value);
|
||||
}
|
||||
|
||||
public int PreviewBackgroundImageTintIntensity
|
||||
{
|
||||
get => (int)GetValue(PreviewBackgroundImageTintIntensityProperty);
|
||||
set => SetValue(PreviewBackgroundImageTintIntensityProperty, value);
|
||||
}
|
||||
|
||||
public Visibility ShowBackgroundImage
|
||||
{
|
||||
get => (Visibility)GetValue(ShowBackgroundImageProperty);
|
||||
set => SetValue(ShowBackgroundImageProperty, value);
|
||||
}
|
||||
|
||||
public CommandPalettePreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not CommandPalettePreview preview)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private double ToOpacity(int value) => value / 100.0;
|
||||
|
||||
private double ToTintIntensity(int value) => value / 100.0;
|
||||
|
||||
private Stretch ToStretch(BackgroundImageFit fit)
|
||||
{
|
||||
return fit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => Stretch.Fill,
|
||||
BackgroundImageFit.UniformToFill => Stretch.UniformToFill,
|
||||
_ => Stretch.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScreenPreview"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border
|
||||
x:Name="ScreenBorder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="Black"
|
||||
BorderThickness="8"
|
||||
CornerRadius="8"
|
||||
UseLayoutRounding="True">
|
||||
<Viewbox Height="120" UseLayoutRounding="True">
|
||||
<Grid>
|
||||
<Image
|
||||
x:Name="WallpaperImage"
|
||||
MaxHeight="200"
|
||||
Stretch="Uniform"
|
||||
UseLayoutRounding="True" />
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Margin="40"
|
||||
Content="{x:Bind PreviewContent}"
|
||||
UseLayoutRounding="True" />
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[ContentProperty(Name = nameof(PreviewContent))]
|
||||
public sealed partial class ScreenPreview : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty PreviewContentProperty =
|
||||
DependencyProperty.Register(nameof(PreviewContent), typeof(object), typeof(ScreenPreview), new PropertyMetadata(null!))!;
|
||||
|
||||
public object PreviewContent
|
||||
{
|
||||
get => GetValue(PreviewContentProperty)!;
|
||||
set => SetValue(PreviewContentProperty, value);
|
||||
}
|
||||
|
||||
public ScreenPreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var wallpaperHelper = new WallpaperHelper();
|
||||
WallpaperImage!.Source = wallpaperHelper.GetWallpaperImage()!;
|
||||
ScreenBorder!.Background = new SolidColorBrush(wallpaperHelper.GetWallpaperColor());
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUi="using:Microsoft.CmdPal.UI"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -22,6 +21,7 @@
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
h:TextBoxCaretColor.SyncWithForeground="True"
|
||||
AutomationProperties.AutomationId="MainSearchBox"
|
||||
KeyDown="FilterBox_KeyDown"
|
||||
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"
|
||||
|
||||
@@ -7,8 +7,10 @@ using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -49,6 +51,8 @@ public sealed partial class SearchBar : UserControl,
|
||||
// 0.6+ suggestions
|
||||
private string? _textToSuggest;
|
||||
|
||||
private SettingsModel Settings => App.Current.Services.GetRequiredService<SettingsModel>();
|
||||
|
||||
public PageViewModel? CurrentPageViewModel
|
||||
{
|
||||
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
|
||||
@@ -131,20 +135,39 @@ public sealed partial class SearchBar : UserControl,
|
||||
}
|
||||
else if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilterBox.Text))
|
||||
switch (Settings.EscapeKeyBehaviorSetting)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear the search box
|
||||
FilterBox.Text = string.Empty;
|
||||
case EscapeKeyBehavior.AlwaysGoBack:
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
break;
|
||||
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
case EscapeKeyBehavior.AlwaysDismiss:
|
||||
WeakReferenceMessenger.Default.Send<DismissMessage>(new(ForceGoHome: true));
|
||||
break;
|
||||
|
||||
case EscapeKeyBehavior.AlwaysHide:
|
||||
WeakReferenceMessenger.Default.Send<HideWindowMessage>(new());
|
||||
break;
|
||||
|
||||
case EscapeKeyBehavior.ClearSearchFirstThenGoBack:
|
||||
default:
|
||||
if (string.IsNullOrEmpty(FilterBox.Text))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear the search box
|
||||
FilterBox.Text = string.Empty;
|
||||
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
@@ -185,21 +208,32 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Left)
|
||||
{
|
||||
// Check if we're in a grid view, and if so, send grid navigation command
|
||||
var isGridView = CurrentPageViewModel is ListViewModel { IsGridView: true };
|
||||
|
||||
// Special handling is required if we're in grid view.
|
||||
if (isGridView)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateLeftCommand>();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
else if (e.Key == VirtualKey.Right)
|
||||
{
|
||||
// Check if the "replace search text with suggestion" feature from 0.4-0.5 is enabled.
|
||||
// If it isn't, then only use the suggestion when the caret is at the end of the input.
|
||||
if (!IsTextToSuggestEnabled)
|
||||
{
|
||||
if (_textToSuggest != null &&
|
||||
if (!string.IsNullOrEmpty(_textToSuggest) &&
|
||||
FilterBox.SelectionStart == FilterBox.Text.Length)
|
||||
{
|
||||
FilterBox.Text = _textToSuggest;
|
||||
FilterBox.Select(_textToSuggest.Length, 0);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Here, we're using the "replace search text with suggestion" feature.
|
||||
@@ -209,6 +243,20 @@ public sealed partial class SearchBar : UserControl,
|
||||
_lastText = null;
|
||||
DoFilterBoxUpdate();
|
||||
}
|
||||
|
||||
// Wouldn't want to perform text completion *and* move the selected item, so only perform this if text suggestion wasn't performed.
|
||||
if (!e.Handled)
|
||||
{
|
||||
// Check if we're in a grid view, and if so, send grid navigation command
|
||||
var isGridView = CurrentPageViewModel is ListViewModel { IsGridView: true };
|
||||
|
||||
// Special handling is required if we're in grid view.
|
||||
if (isGridView)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateRightCommand>();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.Key == VirtualKey.Down)
|
||||
{
|
||||
@@ -251,6 +299,8 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
e.Key == VirtualKey.Up ||
|
||||
e.Key == VirtualKey.Down ||
|
||||
e.Key == VirtualKey.Left ||
|
||||
e.Key == VirtualKey.Right ||
|
||||
|
||||
e.Key == VirtualKey.RightMenu ||
|
||||
e.Key == VirtualKey.LeftMenu ||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
internal sealed class UVBounds
|
||||
{
|
||||
public double UMin { get; }
|
||||
|
||||
public double UMax { get; }
|
||||
|
||||
public double VMin { get; }
|
||||
|
||||
public double VMax { get; }
|
||||
|
||||
public UVBounds(Orientation orientation, Rect rect)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
UMin = rect.Left;
|
||||
UMax = rect.Right;
|
||||
VMin = rect.Top;
|
||||
VMax = rect.Bottom;
|
||||
}
|
||||
else
|
||||
{
|
||||
UMin = rect.Top;
|
||||
UMax = rect.Bottom;
|
||||
VMin = rect.Left;
|
||||
VMax = rect.Right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[DebuggerDisplay("U = {U} V = {V}")]
|
||||
internal struct UvMeasure
|
||||
{
|
||||
internal double U { get; set; }
|
||||
|
||||
internal double V { get; set; }
|
||||
|
||||
internal static UvMeasure Zero => default(UvMeasure);
|
||||
|
||||
public UvMeasure(Orientation orientation, Size size)
|
||||
: this(orientation, size.Width, size.Height)
|
||||
{
|
||||
}
|
||||
|
||||
public UvMeasure(Orientation orientation, double width, double height)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
U = width;
|
||||
V = height;
|
||||
}
|
||||
else
|
||||
{
|
||||
U = height;
|
||||
V = width;
|
||||
}
|
||||
}
|
||||
|
||||
public UvMeasure Add(double u, double v)
|
||||
{
|
||||
UvMeasure result = default(UvMeasure);
|
||||
result.U = U + u;
|
||||
result.V = V + v;
|
||||
return result;
|
||||
}
|
||||
|
||||
public UvMeasure Add(UvMeasure measure)
|
||||
{
|
||||
return Add(measure.U, measure.V);
|
||||
}
|
||||
|
||||
public Size ToSize(Orientation orientation)
|
||||
{
|
||||
if (orientation != Orientation.Horizontal)
|
||||
{
|
||||
return new Size(V, U);
|
||||
}
|
||||
|
||||
return new Size(U, V);
|
||||
}
|
||||
|
||||
public Point GetPoint(Orientation orientation)
|
||||
{
|
||||
return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U);
|
||||
}
|
||||
|
||||
public Size GetSize(Orientation orientation)
|
||||
{
|
||||
return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U);
|
||||
}
|
||||
|
||||
public static bool operator ==(UvMeasure measure1, UvMeasure measure2)
|
||||
{
|
||||
return measure1.U == measure2.U && measure1.V == measure2.V;
|
||||
}
|
||||
|
||||
public static bool operator !=(UvMeasure measure1, UvMeasure measure2)
|
||||
{
|
||||
return !(measure1 == measure2);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is UvMeasure measure && this == measure;
|
||||
}
|
||||
|
||||
public bool Equals(UvMeasure value)
|
||||
{
|
||||
return this == value;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Arranges elements by wrapping them to fit the available space.
|
||||
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
|
||||
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
|
||||
/// </summary>
|
||||
public sealed partial class WrapPanel : Panel
|
||||
{
|
||||
private struct UvRect
|
||||
{
|
||||
public UvMeasure Position { get; set; }
|
||||
|
||||
public UvMeasure Size { get; set; }
|
||||
|
||||
public Rect ToRect(Orientation orientation)
|
||||
{
|
||||
return orientation switch
|
||||
{
|
||||
Orientation.Vertical => new Rect(Position.V, Position.U, Size.V, Size.U),
|
||||
Orientation.Horizontal => new Rect(Position.U, Position.V, Size.U, Size.V),
|
||||
_ => ThrowArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
private static Rect ThrowArgumentException()
|
||||
{
|
||||
throw new ArgumentException("The input orientation is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
private struct Row
|
||||
{
|
||||
public List<UvRect> ChildrenRects { get; }
|
||||
|
||||
public UvMeasure Size { get; set; }
|
||||
|
||||
public UvRect Rect
|
||||
{
|
||||
get
|
||||
{
|
||||
UvRect result;
|
||||
if (ChildrenRects.Count <= 0)
|
||||
{
|
||||
result = default(UvRect);
|
||||
result.Position = UvMeasure.Zero;
|
||||
result.Size = Size;
|
||||
return result;
|
||||
}
|
||||
|
||||
result = default(UvRect);
|
||||
result.Position = ChildrenRects.First().Position;
|
||||
result.Size = Size;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Row(List<UvRect> childrenRects, UvMeasure size)
|
||||
{
|
||||
ChildrenRects = childrenRects;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public void Add(UvMeasure position, UvMeasure size)
|
||||
{
|
||||
ChildrenRects.Add(new UvRect
|
||||
{
|
||||
Position = position,
|
||||
Size = size,
|
||||
});
|
||||
|
||||
Size = new UvMeasure
|
||||
{
|
||||
U = position.U + size.U,
|
||||
V = Math.Max(Size.V, size.V),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
|
||||
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
|
||||
/// </summary>
|
||||
public double HorizontalSpacing
|
||||
{
|
||||
get { return (double)GetValue(HorizontalSpacingProperty); }
|
||||
set { SetValue(HorizontalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty HorizontalSpacingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(HorizontalSpacing),
|
||||
typeof(double),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
|
||||
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
|
||||
/// </summary>
|
||||
public double VerticalSpacing
|
||||
{
|
||||
get { return (double)GetValue(VerticalSpacingProperty); }
|
||||
set { SetValue(VerticalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty VerticalSpacingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(VerticalSpacing),
|
||||
typeof(double),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(0d, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orientation of the WrapPanel.
|
||||
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
|
||||
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
|
||||
/// </summary>
|
||||
public Orientation Orientation
|
||||
{
|
||||
get { return (Orientation)GetValue(OrientationProperty); }
|
||||
set { SetValue(OrientationProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Orientation"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Orientation),
|
||||
typeof(Orientation),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distance between the border and its child object.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The dimensions of the space between the border and its child as a Thickness value.
|
||||
/// Thickness is a structure that stores dimension values using pixel measures.
|
||||
/// </returns>
|
||||
public Thickness Padding
|
||||
{
|
||||
get { return (Thickness)GetValue(PaddingProperty); }
|
||||
set { SetValue(PaddingProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the Padding dependency property.
|
||||
/// </summary>
|
||||
/// <returns>The identifier for the <see cref="Padding"/> dependency property.</returns>
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Padding),
|
||||
typeof(Thickness),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(default(Thickness), LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating how to arrange child items
|
||||
/// </summary>
|
||||
public StretchChild StretchChild
|
||||
{
|
||||
get { return (StretchChild)GetValue(StretchChildProperty); }
|
||||
set { SetValue(StretchChildProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="StretchChild"/> dependency property.
|
||||
/// </summary>
|
||||
/// <returns>The identifier for the <see cref="StretchChild"/> dependency property.</returns>
|
||||
public static readonly DependencyProperty StretchChildProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(StretchChild),
|
||||
typeof(StretchChild),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(StretchChild.None, LayoutPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the IsFullLine attached dependency property.
|
||||
/// If true, the child element will occupy the entire width of the panel and force a line break before and after itself.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsFullLineProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsFullLine",
|
||||
typeof(bool),
|
||||
typeof(WrapPanel),
|
||||
new PropertyMetadata(false, OnIsFullLineChanged));
|
||||
|
||||
public static bool GetIsFullLine(DependencyObject obj)
|
||||
{
|
||||
return (bool)obj.GetValue(IsFullLineProperty);
|
||||
}
|
||||
|
||||
public static void SetIsFullLine(DependencyObject obj, bool value)
|
||||
{
|
||||
obj.SetValue(IsFullLineProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsFullLineChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (FindVisualParentWrapPanel(d) is WrapPanel wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
private static WrapPanel? FindVisualParentWrapPanel(DependencyObject child)
|
||||
{
|
||||
var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(child);
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is WrapPanel wrapPanel)
|
||||
{
|
||||
return wrapPanel;
|
||||
}
|
||||
|
||||
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WrapPanel wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
wp.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Row> _rows = new List<Row>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var childAvailableSize = new Size(
|
||||
availableSize.Width - Padding.Left - Padding.Right,
|
||||
availableSize.Height - Padding.Top - Padding.Bottom);
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Measure(childAvailableSize);
|
||||
}
|
||||
|
||||
var requiredSize = UpdateRows(availableSize);
|
||||
return requiredSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if ((Orientation == Orientation.Horizontal && finalSize.Width < DesiredSize.Width) ||
|
||||
(Orientation == Orientation.Vertical && finalSize.Height < DesiredSize.Height))
|
||||
{
|
||||
// We haven't received our desired size. We need to refresh the rows.
|
||||
UpdateRows(finalSize);
|
||||
}
|
||||
|
||||
if (_rows.Count > 0)
|
||||
{
|
||||
// Now that we have all the data, we do the actual arrange pass
|
||||
var childIndex = 0;
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
foreach (var rect in row.ChildrenRects)
|
||||
{
|
||||
var child = Children[childIndex++];
|
||||
while (child.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
// Collapsed children are not added into the rows,
|
||||
// we skip them.
|
||||
child = Children[childIndex++];
|
||||
}
|
||||
|
||||
var arrangeRect = new UvRect
|
||||
{
|
||||
Position = rect.Position,
|
||||
Size = new UvMeasure { U = rect.Size.U, V = row.Size.V },
|
||||
};
|
||||
|
||||
var finalRect = arrangeRect.ToRect(Orientation);
|
||||
child.Arrange(finalRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private Size UpdateRows(Size availableSize)
|
||||
{
|
||||
_rows.Clear();
|
||||
|
||||
var paddingStart = new UvMeasure(Orientation, Padding.Left, Padding.Top);
|
||||
var paddingEnd = new UvMeasure(Orientation, Padding.Right, Padding.Bottom);
|
||||
|
||||
if (Children.Count == 0)
|
||||
{
|
||||
return paddingStart.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
|
||||
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
|
||||
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
|
||||
var position = new UvMeasure(Orientation, Padding.Left, Padding.Top);
|
||||
|
||||
var currentRow = new Row(new List<UvRect>(), default);
|
||||
var finalMeasure = new UvMeasure(Orientation, width: 0.0, height: 0.0);
|
||||
|
||||
void CommitRow()
|
||||
{
|
||||
// Only adds if the row has a content
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
_rows.Add(currentRow);
|
||||
|
||||
position.V += currentRow.Size.V + spacingMeasure.V;
|
||||
}
|
||||
|
||||
position.U = paddingStart.U;
|
||||
|
||||
currentRow = new Row(new List<UvRect>(), default);
|
||||
}
|
||||
|
||||
void Arrange(UIElement child, bool isLast = false)
|
||||
{
|
||||
if (child.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isFullLine = IsSectionItem(child);
|
||||
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
|
||||
|
||||
if (isFullLine)
|
||||
{
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
CommitRow();
|
||||
}
|
||||
|
||||
// Forces the width to fill all the available space
|
||||
// (Total width - Padding Left - Padding Right)
|
||||
desiredMeasure.U = parentMeasure.U - paddingStart.U - paddingEnd.U;
|
||||
|
||||
// Adds the Section Header to the row
|
||||
currentRow.Add(position, desiredMeasure);
|
||||
|
||||
// Updates the global measures
|
||||
position.U += desiredMeasure.U + spacingMeasure.U;
|
||||
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
|
||||
|
||||
CommitRow();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Checks if the item can fit in the row
|
||||
if ((desiredMeasure.U + position.U + paddingEnd.U) > parentMeasure.U)
|
||||
{
|
||||
CommitRow();
|
||||
}
|
||||
|
||||
if (isLast)
|
||||
{
|
||||
desiredMeasure.U = parentMeasure.U - position.U;
|
||||
}
|
||||
|
||||
currentRow.Add(position, desiredMeasure);
|
||||
|
||||
position.U += desiredMeasure.U + spacingMeasure.U;
|
||||
finalMeasure.U = Math.Max(finalMeasure.U, position.U);
|
||||
}
|
||||
}
|
||||
|
||||
var lastIndex = Children.Count - 1;
|
||||
for (var i = 0; i < lastIndex; i++)
|
||||
{
|
||||
Arrange(Children[i]);
|
||||
}
|
||||
|
||||
Arrange(Children[lastIndex], StretchChild == StretchChild.Last);
|
||||
|
||||
if (currentRow.ChildrenRects.Count > 0)
|
||||
{
|
||||
_rows.Add(currentRow);
|
||||
}
|
||||
|
||||
if (_rows.Count == 0)
|
||||
{
|
||||
return paddingStart.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
|
||||
var lastRowRect = _rows.Last().Rect;
|
||||
finalMeasure.V = lastRowRect.Position.V + lastRowRect.Size.V;
|
||||
return finalMeasure.Add(paddingEnd).ToSize(Orientation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a color, either black or white, depending on the brightness of the supplied color.
|
||||
/// </summary>
|
||||
public sealed partial class ContrastBrushConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the alpha channel threshold below which a default color is used instead of black/white.
|
||||
/// </summary>
|
||||
public byte AlphaThreshold { get; set; } = 128;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object Convert(
|
||||
object value,
|
||||
Type targetType,
|
||||
object parameter,
|
||||
string language)
|
||||
{
|
||||
Color comparisonColor;
|
||||
Color? defaultColor = null;
|
||||
|
||||
// Get the changing color to compare against
|
||||
if (value is Color valueColor)
|
||||
{
|
||||
comparisonColor = valueColor;
|
||||
}
|
||||
else if (value is SolidColorBrush valueBrush)
|
||||
{
|
||||
comparisonColor = valueBrush.Color;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid color value provided
|
||||
return DependencyProperty.UnsetValue;
|
||||
}
|
||||
|
||||
// Get the default color when transparency is high
|
||||
if (parameter is Color parameterColor)
|
||||
{
|
||||
defaultColor = parameterColor;
|
||||
}
|
||||
else if (parameter is SolidColorBrush parameterBrush)
|
||||
{
|
||||
defaultColor = parameterBrush.Color;
|
||||
}
|
||||
|
||||
if (comparisonColor.A < AlphaThreshold &&
|
||||
defaultColor.HasValue)
|
||||
{
|
||||
// If the transparency is less than 50 %, just use the default brush
|
||||
// This can commonly be something like the TextControlForeground brush
|
||||
return new SolidColorBrush(defaultColor.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Chose a white/black brush based on contrast to the base color
|
||||
return UseLightContrastColor(comparisonColor)
|
||||
? new SolidColorBrush(Colors.White)
|
||||
: new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object ConvertBack(
|
||||
object value,
|
||||
Type targetType,
|
||||
object parameter,
|
||||
string language)
|
||||
{
|
||||
return DependencyProperty.UnsetValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a light or dark contrast color should be used with the given displayed color.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This code is using the WinUI algorithm.
|
||||
/// </remarks>
|
||||
private bool UseLightContrastColor(Color displayedColor)
|
||||
{
|
||||
// The selection ellipse should be light if and only if the chosen color
|
||||
// contrasts more with black than it does with white.
|
||||
// To find how much something contrasts with white, we use the equation
|
||||
// for relative luminance, which is given by
|
||||
//
|
||||
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
|
||||
//
|
||||
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
|
||||
//
|
||||
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
|
||||
// then the color is closer to black. This is based on the fact that the human
|
||||
// eye perceives green to be much brighter than red, which in turn is perceived to be
|
||||
// brighter than blue.
|
||||
//
|
||||
// If the third dimension is value, then we won't be updating the spectrum's displayed colors,
|
||||
// so in that case we should use a value of 1 when considering the backdrop
|
||||
// for the selection ellipse.
|
||||
var rg = displayedColor.R <= 10
|
||||
? displayedColor.R / 3294.0
|
||||
: Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4);
|
||||
var gg = displayedColor.G <= 10
|
||||
? displayedColor.G / 3294.0
|
||||
: Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4);
|
||||
var bg = displayedColor.B <= 10
|
||||
? displayedColor.B / 3294.0
|
||||
: Math.Pow((displayedColor.B / 269.0) + 0.0513, 2.4);
|
||||
|
||||
return (0.2126 * rg) + (0.7152 * gg) + (0.0722 * bg) <= 0.5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public partial class DetailsSizeToGridLengthConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ContentSize size)
|
||||
{
|
||||
// This converter calculates the Star width for the LIST.
|
||||
//
|
||||
// The input 'size' (ContentSize) represents the TARGET WIDTH desired for the DETAILS PANEL.
|
||||
//
|
||||
// To ensure the Details Panel achieves its target size (e.g. ContentSize.Large),
|
||||
// we must shrink the List and let the Details fill the available space.
|
||||
// (e.g., A larger target size for Details results in a smaller Star value for the List).
|
||||
var starValue = size switch
|
||||
{
|
||||
ContentSize.Small => 3.0,
|
||||
ContentSize.Medium => 2.0,
|
||||
ContentSize.Large => 1.0,
|
||||
_ => 3.0,
|
||||
};
|
||||
|
||||
return new GridLength(starValue, GridUnitType.Star);
|
||||
}
|
||||
|
||||
return new GridLength(3.0, GridUnitType.Star);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
@@ -18,8 +18,23 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
||||
|
||||
public DataTemplate? Gallery { get; set; }
|
||||
|
||||
public DataTemplate? Section { get; set; }
|
||||
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
if (item is ListItemViewModel element && element.IsSectionOrSeparator)
|
||||
{
|
||||
if (dependencyObject is UIElement li)
|
||||
{
|
||||
li.IsTabStop = false;
|
||||
li.IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
{
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public sealed partial class ListItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? ListItem { get; set; }
|
||||
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
public DataTemplate? Section { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
DataTemplate? dataTemplate = ListItem;
|
||||
|
||||
if (container is ListViewItem listItem)
|
||||
{
|
||||
if (item is ListItemViewModel element)
|
||||
{
|
||||
if (container is ListViewItem li && element.IsSectionOrSeparator)
|
||||
{
|
||||
li.IsEnabled = false;
|
||||
li.AllowFocusWhenDisabled = false;
|
||||
li.AllowFocusOnInteraction = false;
|
||||
li.IsHitTestVisible = false;
|
||||
dataTemplate = string.IsNullOrWhiteSpace(element.Section) ? Separator : Section;
|
||||
}
|
||||
else
|
||||
{
|
||||
listItem.IsEnabled = true;
|
||||
listItem.AllowFocusWhenDisabled = true;
|
||||
listItem.AllowFocusOnInteraction = true;
|
||||
listItem.IsHitTestVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
@@ -90,6 +92,8 @@
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
@@ -168,8 +172,17 @@
|
||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:GridItemContainerStyleSelector
|
||||
x:Key="GridItemContainerStyleSelector"
|
||||
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
||||
@@ -241,12 +254,46 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Margin="0,2,0,2"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSectionViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
cpcontrols:WrapPanel.IsFullLine="True"
|
||||
ColumnSpacing="8"
|
||||
IsTabStop="False"
|
||||
IsTapEnabled="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Foreground="{ThemeResource TextFillColorDisabled}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Section}" />
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Grid item templates for visual grid representation -->
|
||||
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="60"
|
||||
Height="60"
|
||||
Padding="8,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
@@ -265,7 +312,6 @@
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -393,13 +439,16 @@
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
CanDragItems="True"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
DragItemsCompleted="Items_DragItemsCompleted"
|
||||
DragItemsStarting="Items_DragItemsStarting"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
@@ -411,10 +460,13 @@
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="8"
|
||||
Padding="16,0"
|
||||
CanDragItems="True"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
DragItemsCompleted="Items_DragItemsCompleted"
|
||||
DragItemsStarting="Items_DragItemsStarting"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
@@ -423,10 +475,14 @@
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</GridView.ItemContainerTransitions>
|
||||
<GridView.ItemContainerStyle />
|
||||
</GridView>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
|
||||
@@ -26,6 +27,8 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class ListPage : Page,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<NavigateLeftCommand>,
|
||||
IRecipient<NavigateRightCommand>,
|
||||
IRecipient<NavigatePageDownCommand>,
|
||||
IRecipient<NavigatePageUpCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
@@ -74,17 +77,25 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
ViewModel = listViewModel;
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
if (e.NavigationMode == NavigationMode.Back)
|
||||
{
|
||||
// Upon navigating _back_ to this page, immediately select the
|
||||
// first item in the list
|
||||
ItemView.SelectedIndex = 0;
|
||||
// Must dispatch the selection to run at a lower priority; otherwise, GetFirstSelectableIndex
|
||||
// may return an incorrect index because item containers are not yet rendered.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateLeftCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateRightCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
@@ -99,6 +110,8 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateLeftCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateRightCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||
@@ -122,6 +135,29 @@ public sealed partial class ListPage : Page,
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of the first item in the list that is not a separator.
|
||||
/// Returns -1 if the list is empty or only contains separators.
|
||||
/// </summary>
|
||||
private int GetFirstSelectableIndex()
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (!IsSeparator(items[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
@@ -177,19 +213,33 @@ public sealed partial class ListPage : Page,
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null)
|
||||
if (ItemView.SelectedItem is not null && !IsSeparator(ItemView.SelectedItem))
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
var items = ItemView.Items;
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
var shouldScroll = false;
|
||||
|
||||
if (e.RemovedItems.Count > 0)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
else if (ItemView.SelectedIndex > firstUsefulIndex)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
|
||||
if (shouldScroll)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
li.Title,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
@@ -257,25 +307,56 @@ public sealed partial class ListPage : Page,
|
||||
// And then have these commands manipulate that state being bound to the UI instead
|
||||
// We may want to see how other non-list UIs need to behave to make this decision
|
||||
// At least it's decoupled from the SearchBox now :)
|
||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
ItemView.SelectedIndex++;
|
||||
// For grid views, use spatial navigation (down)
|
||||
HandleGridArrowNavigation(VirtualKey.Down);
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
// For list views, use simple linear navigation
|
||||
NavigateDown();
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePreviousCommand message)
|
||||
{
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
ItemView.SelectedIndex--;
|
||||
// For grid views, use spatial navigation (up)
|
||||
HandleGridArrowNavigation(VirtualKey.Up);
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||
NavigateUp();
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigateLeftCommand message)
|
||||
{
|
||||
// For grid views, use spatial navigation. For list views, just move up.
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
HandleGridArrowNavigation(VirtualKey.Left);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In list view, left arrow doesn't navigate
|
||||
// This maintains consistency with the SearchBar behavior
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigateRightCommand message)
|
||||
{
|
||||
// For grid views, use spatial navigation. For list views, just move down.
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
HandleGridArrowNavigation(VirtualKey.Right);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In list view, right arrow doesn't navigate
|
||||
// This maintains consistency with the SearchBar behavior
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +395,10 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +413,10 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,17 +559,65 @@ public sealed partial class ListPage : Page,
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemView.SelectedItem is null)
|
||||
// The selection logic must be deferred to the DispatcherQueue
|
||||
// to ensure the UI has processed the updated ItemsSource binding,
|
||||
// preventing ItemView.Items from appearing empty/null immediately after update.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
var items = ItemView.Items;
|
||||
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
// If the list is null or empty, clears the selection and return
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Finds the first item that is not a separator
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
|
||||
// If there is only separators in the list, don't select anything.
|
||||
if (firstUsefulIndex == -1)
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldUpdateSelection = false;
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// No current selection or current selection is null
|
||||
else if (ItemView.SelectedItem is null)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The current selected item is a separator
|
||||
else if (IsSeparator(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The selected item does not exist in the new list
|
||||
else if (!items.Contains(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
if (shouldUpdateSelection)
|
||||
{
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@@ -514,6 +649,135 @@ public sealed partial class ListPage : Page,
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find a logical neighbor in the requested direction using containers' positions.
|
||||
private void HandleGridArrowNavigation(VirtualKey key)
|
||||
{
|
||||
if (ItemView.Items.Count == 0)
|
||||
{
|
||||
// No items, goodbye.
|
||||
return;
|
||||
}
|
||||
|
||||
var currentIndex = ItemView.SelectedIndex;
|
||||
if (currentIndex < 0)
|
||||
{
|
||||
// -1 is a valid value (no item currently selected)
|
||||
currentIndex = 0;
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to compute using container positions; if not available, fall back to simple +/-1.
|
||||
var currentContainer = ItemView.ContainerFromIndex(currentIndex) as FrameworkElement;
|
||||
if (currentContainer is not null && currentContainer.ActualWidth != 0 && currentContainer.ActualHeight != 0)
|
||||
{
|
||||
// Use center of current container as reference
|
||||
var curPoint = currentContainer.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||
var curCenterX = curPoint.X + (currentContainer.ActualWidth / 2.0);
|
||||
var curCenterY = curPoint.Y + (currentContainer.ActualHeight / 2.0);
|
||||
|
||||
var bestScore = double.MaxValue;
|
||||
var bestIndex = currentIndex;
|
||||
|
||||
for (var i = 0; i < ItemView.Items.Count; i++)
|
||||
{
|
||||
if (i == currentIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsSeparator(ItemView.Items[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
|
||||
{
|
||||
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||
var centerX = p.X + (c.ActualWidth / 2.0);
|
||||
var centerY = p.Y + (c.ActualHeight / 2.0);
|
||||
|
||||
var dx = centerX - curCenterX;
|
||||
var dy = centerY - curCenterY;
|
||||
|
||||
var candidate = false;
|
||||
var score = double.MaxValue;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case VirtualKey.Left:
|
||||
if (dx < 0)
|
||||
{
|
||||
candidate = true;
|
||||
score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
|
||||
}
|
||||
|
||||
break;
|
||||
case VirtualKey.Right:
|
||||
if (dx > 0)
|
||||
{
|
||||
candidate = true;
|
||||
score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
|
||||
}
|
||||
|
||||
break;
|
||||
case VirtualKey.Up:
|
||||
if (dy < 0)
|
||||
{
|
||||
candidate = true;
|
||||
score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
|
||||
}
|
||||
|
||||
break;
|
||||
case VirtualKey.Down:
|
||||
if (dy > 0)
|
||||
{
|
||||
candidate = true;
|
||||
score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (candidate && score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex != currentIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = bestIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore transform errors and fall back
|
||||
}
|
||||
|
||||
// fallback linear behavior
|
||||
var fallback = key switch
|
||||
{
|
||||
VirtualKey.Left => Math.Max(0, currentIndex - 1),
|
||||
VirtualKey.Right => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
|
||||
VirtualKey.Up => Math.Max(0, currentIndex - 1),
|
||||
VirtualKey.Down => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
|
||||
_ => currentIndex,
|
||||
};
|
||||
if (fallback != currentIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = fallback;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
var (item, element) = e.OriginalSource switch
|
||||
@@ -564,12 +828,209 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
// Track keyboard as the last input source for activation logic.
|
||||
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
||||
{
|
||||
_lastInputSource = InputSource.Keyboard;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle arrow navigation when we're showing a grid.
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case VirtualKey.Left:
|
||||
case VirtualKey.Right:
|
||||
case VirtualKey.Up:
|
||||
case VirtualKey.Down:
|
||||
_lastInputSource = InputSource.Keyboard;
|
||||
HandleGridArrowNavigation(e.Key);
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.NavigateUp"/>
|
||||
/// </summary>
|
||||
private void NavigateUp()
|
||||
{
|
||||
var newIndex = ItemView.SelectedIndex;
|
||||
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
newIndex--;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
if (newIndex < 0)
|
||||
{
|
||||
newIndex = ItemView.Items.Count - 1;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
|
||||
ItemView.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Items.FirstOrDefault() is not ListItemViewModel item || item.DataPackage is null)
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// copy properties
|
||||
foreach (var (key, value) in item.DataPackage.Properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.Data.Properties[key] = value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// noop - skip any properties that fail
|
||||
}
|
||||
}
|
||||
|
||||
// setup e.Data formats as deferred renderers to read from the item's DataPackage
|
||||
foreach (var format in item.DataPackage.AvailableFormats)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.Data.SetDataProvider(format, request => DelayRenderer(request, item, format));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// noop - skip any formats that fail
|
||||
}
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new DragStartedMessage());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
|
||||
Logger.LogError("Failed to start dragging an item", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DelayRenderer(DataProviderRequest request, ListItemViewModel item, string format)
|
||||
{
|
||||
var deferral = request.GetDeferral();
|
||||
try
|
||||
{
|
||||
item.DataPackage?.GetDataAsync(format)
|
||||
.AsTask()
|
||||
.ContinueWith(dataTask =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dataTask.IsCompletedSuccessfully)
|
||||
{
|
||||
request.SetData(dataTask.Result);
|
||||
}
|
||||
else if (dataTask.IsFaulted && dataTask.Exception is not null)
|
||||
{
|
||||
Logger.LogError($"Failed to get data for format '{format}' during drag-and-drop", dataTask.Exception);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set data for format '{format}' during drag-and-drop", ex);
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.NavigateDown"/>
|
||||
/// </summary>
|
||||
private void NavigateDown()
|
||||
{
|
||||
var newIndex = ItemView.SelectedIndex;
|
||||
|
||||
if (ItemView.SelectedIndex == ItemView.Items.Count - 1)
|
||||
{
|
||||
newIndex = 0;
|
||||
while (
|
||||
newIndex < ItemView.Items.Count &&
|
||||
IsSeparator(ItemView.Items[newIndex]))
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
if (newIndex >= ItemView.Items.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex++;
|
||||
|
||||
while (
|
||||
newIndex < ItemView.Items.Count &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
if (newIndex >= ItemView.Items.Count)
|
||||
{
|
||||
newIndex = 0;
|
||||
|
||||
while (
|
||||
newIndex < ItemView.Items.Count &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemView.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.IsSeparator(object)"/>
|
||||
/// </summary>
|
||||
private bool IsSeparator(object? item) => item is ListItemViewModel li && li.IsSectionOrSeparator;
|
||||
|
||||
private enum InputSource
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -10,6 +10,8 @@ internal static class BindTransformers
|
||||
{
|
||||
public static bool Negate(bool value) => !value;
|
||||
|
||||
public static Visibility NegateVisibility(Visibility value) => value == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
public static Visibility EmptyToCollapsed(string? input)
|
||||
=> string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Color"/>.
|
||||
/// </summary>
|
||||
internal static class ColorExtensions
|
||||
{
|
||||
/// <param name="color">Input color.</param>
|
||||
public static double CalculateBrightness(this Color color)
|
||||
{
|
||||
return color.ToHsv().V;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows to change the brightness by a factor based on the HSV color space.
|
||||
/// </summary>
|
||||
/// <param name="color">Input color.</param>
|
||||
/// <param name="brightnessFactor">The brightness adjustment factor, ranging from -1 to 1.</param>
|
||||
/// <returns>Updated color.</returns>
|
||||
public static Color UpdateBrightness(this Color color, double brightnessFactor)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1);
|
||||
|
||||
var hsvColor = color.ToHsv();
|
||||
return ColorHelper.FromHsv(hsvColor.H, hsvColor.S, Math.Clamp(hsvColor.V + brightnessFactor, 0, 1), hsvColor.A);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the color by adjusting brightness, saturation, and luminance factors.
|
||||
/// </summary>
|
||||
/// <param name="color">Input color.</param>
|
||||
/// <param name="brightnessFactor">The brightness adjustment factor, ranging from -1 to 1.</param>
|
||||
/// <param name="saturationFactor">The saturation adjustment factor, ranging from -1 to 1. Defaults to 0.</param>
|
||||
/// <param name="luminanceFactor">The luminance adjustment factor, ranging from -1 to 1. Defaults to 0.</param>
|
||||
/// <returns>Updated color.</returns>
|
||||
public static Color Update(this Color color, double brightnessFactor, double saturationFactor = 0, double luminanceFactor = 0)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1);
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(saturationFactor, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(saturationFactor, -1);
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(luminanceFactor, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(luminanceFactor, -1);
|
||||
|
||||
var hsv = color.ToHsv();
|
||||
|
||||
var rgb = ColorHelper.FromHsv(
|
||||
hsv.H,
|
||||
Clamp01(hsv.S + saturationFactor),
|
||||
Clamp01(hsv.V + brightnessFactor));
|
||||
|
||||
if (luminanceFactor == 0)
|
||||
{
|
||||
return rgb;
|
||||
}
|
||||
|
||||
var hsl = rgb.ToHsl();
|
||||
var lightness = Clamp01(hsl.L + luminanceFactor);
|
||||
return ColorHelper.FromHsl(hsl.H, hsl.S, lightness);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two colors in HSV space.
|
||||
/// Hue is blended along the shortest arc on the color wheel (wrap-aware).
|
||||
/// Saturation, Value, and Alpha are blended linearly.
|
||||
/// </summary>
|
||||
/// <param name="a">Start color.</param>
|
||||
/// <param name="b">End color.</param>
|
||||
/// <param name="t">Interpolation factor in [0,1].</param>
|
||||
/// <returns>Interpolated color.</returns>
|
||||
public static Color LerpHsv(this Color a, Color b, double t)
|
||||
{
|
||||
t = Clamp01(t);
|
||||
|
||||
// Convert to HSV
|
||||
var hslA = a.ToHsv();
|
||||
var hslB = b.ToHsv();
|
||||
|
||||
var h1 = hslA.H;
|
||||
var h2 = hslB.H;
|
||||
|
||||
// Handle near-gray hues (undefined hue) by inheriting the other's hue
|
||||
const double satEps = 1e-4f;
|
||||
if (hslA.S < satEps && hslB.S >= satEps)
|
||||
{
|
||||
h1 = h2;
|
||||
}
|
||||
else if (hslB.S < satEps && hslA.S >= satEps)
|
||||
{
|
||||
h2 = h1;
|
||||
}
|
||||
|
||||
return ColorHelper.FromHsv(
|
||||
hue: LerpHueDegrees(h1, h2, t),
|
||||
saturation: Lerp(hslA.S, hslB.S, t),
|
||||
value: Lerp(hslA.V, hslB.V, t),
|
||||
alpha: (byte)Math.Round(Lerp(hslA.A, hslB.A, t)));
|
||||
}
|
||||
|
||||
private static double LerpHueDegrees(double a, double b, double t)
|
||||
{
|
||||
a = Mod360(a);
|
||||
b = Mod360(b);
|
||||
var delta = ((b - a + 540f) % 360f) - 180f;
|
||||
return Mod360(a + (delta * t));
|
||||
}
|
||||
|
||||
private static double Mod360(double angle)
|
||||
{
|
||||
angle %= 360f;
|
||||
if (angle < 0f)
|
||||
{
|
||||
angle += 360f;
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
private static double Lerp(double a, double b, double t) => a + ((b - a) * t);
|
||||
|
||||
private static double Clamp01(double x) => Math.Clamp(x, 0, 1);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user