mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 03:07:56 +01:00
Merge main into yuleng/display/pr/3
Resolve conflict in LightSwitchStateManager.cpp by keeping NotifyPowerDisplay function for PowerDisplay integration.
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
|
||||
|
||||
9
.github/actions/spell-check/expect.txt
vendored
9
.github/actions/spell-check/expect.txt
vendored
@@ -146,8 +146,11 @@ BITSPIXEL
|
||||
bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
Blockquotes
|
||||
blt
|
||||
bluelightreduction
|
||||
bluelightreductionstate
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
bmi
|
||||
@@ -259,6 +262,7 @@ colorformat
|
||||
colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -1156,6 +1160,7 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
nightlight
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1905,6 +1910,8 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
@@ -1916,8 +1923,10 @@ Uniquifies
|
||||
unitconverter
|
||||
unittests
|
||||
UNLEN
|
||||
Uninitializes
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
|
||||
@@ -353,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
|
||||
|
||||
@@ -40,6 +40,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" />
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Configures global PowerToys settings to enable only specified modules and disable all others.
|
||||
/// </summary>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
|
||||
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
|
||||
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modulesToEnable);
|
||||
modulesToEnable ??= Array.Empty<string>();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -62,6 +64,24 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -179,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;
|
||||
|
||||
|
||||
@@ -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,412 @@
|
||||
// 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 Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
|
||||
{
|
||||
_imageBrush ??= _compositor?.CreateSurfaceBrush();
|
||||
if (_imageBrush is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
loadedSurface.LoadCompleted += (_, _) =>
|
||||
{
|
||||
if (_imageBrush is not null)
|
||||
{
|
||||
_imageBrush.Surface = loadedSurface;
|
||||
_imageBrush.Stretch = ConvertStretch(ImageStretch);
|
||||
_imageBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
|
||||
}
|
||||
};
|
||||
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -399,7 +445,7 @@
|
||||
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,7 +457,7 @@
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="8"
|
||||
Padding="16,0"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
@@ -423,10 +469,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>
|
||||
|
||||
@@ -76,12 +76,18 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
ViewModel = listViewModel;
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
if (e.NavigationMode == NavigationMode.Back)
|
||||
{
|
||||
// Upon navigating _back_ to this page, immediately select the
|
||||
// first item in the list
|
||||
ItemView.SelectedIndex = 0;
|
||||
// Must dispatch the selection to run at a lower priority; otherwise, GetFirstSelectableIndex
|
||||
// may return an incorrect index because item containers are not yet rendered.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
@@ -128,6 +134,29 @@ public sealed partial class ListPage : Page,
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of the first item in the list that is not a separator.
|
||||
/// Returns -1 if the list is empty or only contains separators.
|
||||
/// </summary>
|
||||
private int GetFirstSelectableIndex()
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (!IsSeparator(items[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
@@ -183,19 +212,33 @@ public sealed partial class ListPage : Page,
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null)
|
||||
if (ItemView.SelectedItem is not null && !IsSeparator(ItemView.SelectedItem))
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
var items = ItemView.Items;
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
var shouldScroll = false;
|
||||
|
||||
if (e.RemovedItems.Count > 0)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
else if (ItemView.SelectedIndex > firstUsefulIndex)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
|
||||
if (shouldScroll)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
li.Title,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
@@ -271,14 +314,7 @@ public sealed partial class ListPage : Page,
|
||||
else
|
||||
{
|
||||
// For list views, use simple linear navigation
|
||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||
{
|
||||
ItemView.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
NavigateDown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,15 +327,7 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
else
|
||||
{
|
||||
// For list views, use simple linear navigation
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
ItemView.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
NavigateUp();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +394,10 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +412,10 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,17 +558,65 @@ public sealed partial class ListPage : Page,
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemView.SelectedItem is null)
|
||||
// The selection logic must be deferred to the DispatcherQueue
|
||||
// to ensure the UI has processed the updated ItemsSource binding,
|
||||
// preventing ItemView.Items from appearing empty/null immediately after update.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
var items = ItemView.Items;
|
||||
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
}
|
||||
// If the list is null or empty, clears the selection and return
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Finds the first item that is not a separator
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
|
||||
// If there is only separators in the list, don't select anything.
|
||||
if (firstUsefulIndex == -1)
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldUpdateSelection = false;
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// No current selection or current selection is null
|
||||
else if (ItemView.SelectedItem is null)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The current selected item is a separator
|
||||
else if (IsSeparator(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The selected item does not exist in the new list
|
||||
else if (!items.Contains(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
if (shouldUpdateSelection)
|
||||
{
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@@ -604,6 +686,11 @@ public sealed partial class ListPage : Page,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsSeparator(ItemView.Items[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
|
||||
{
|
||||
var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
|
||||
@@ -764,6 +851,102 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code stealed from <see cref="Controls.ContextMenu.NavigateUp"/>
|
||||
/// </summary>
|
||||
private void NavigateUp()
|
||||
{
|
||||
var newIndex = ItemView.SelectedIndex;
|
||||
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
{
|
||||
newIndex--;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
if (newIndex < 0)
|
||||
{
|
||||
newIndex = ItemView.Items.Count - 1;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(ItemView.Items[newIndex]) &&
|
||||
newIndex != ItemView.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = ItemView.Items.Count - 1;
|
||||
}
|
||||
|
||||
ItemView.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Rectangle = Microsoft.UI.Xaml.Shapes.Rectangle;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Attached property to color internal caret/overlay rectangles inside a TextBox
|
||||
/// so they follow the TextBox's actual Foreground brush.
|
||||
/// </summary>
|
||||
public static class TextBoxCaretColor
|
||||
{
|
||||
public static readonly DependencyProperty SyncWithForegroundProperty =
|
||||
DependencyProperty.RegisterAttached("SyncWithForeground", typeof(bool), typeof(TextBoxCaretColor), new PropertyMetadata(false, OnSyncCaretRectanglesChanged))!;
|
||||
|
||||
private static readonly ConditionalWeakTable<TextBox, State> States = [];
|
||||
|
||||
public static void SetSyncWithForeground(DependencyObject obj, bool value)
|
||||
{
|
||||
obj.SetValue(SyncWithForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static bool GetSyncWithForeground(DependencyObject obj)
|
||||
{
|
||||
return (bool)obj.GetValue(SyncWithForegroundProperty);
|
||||
}
|
||||
|
||||
private static void OnSyncCaretRectanglesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not TextBox tb)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bool)e.NewValue)
|
||||
{
|
||||
Attach(tb);
|
||||
}
|
||||
else
|
||||
{
|
||||
Detach(tb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Attach(TextBox tb)
|
||||
{
|
||||
if (States.TryGetValue(tb, out var st) && st.IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
st ??= new State();
|
||||
st.IsHooked = true;
|
||||
States.Remove(tb);
|
||||
States.Add(tb, st);
|
||||
|
||||
tb.Loaded += TbOnLoaded;
|
||||
tb.Unloaded += TbOnUnloaded;
|
||||
tb.GotFocus += TbOnGotFocus;
|
||||
|
||||
st.ForegroundToken = tb.RegisterPropertyChangedCallback(Control.ForegroundProperty!, (_, _) => Apply(tb));
|
||||
|
||||
if (tb.IsLoaded)
|
||||
{
|
||||
Apply(tb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Detach(TextBox tb)
|
||||
{
|
||||
if (!States.TryGetValue(tb, out var st))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tb.Loaded -= TbOnLoaded;
|
||||
tb.Unloaded -= TbOnUnloaded;
|
||||
tb.GotFocus -= TbOnGotFocus;
|
||||
|
||||
if (st.ForegroundToken != 0)
|
||||
{
|
||||
tb.UnregisterPropertyChangedCallback(Control.ForegroundProperty!, st.ForegroundToken);
|
||||
st.ForegroundToken = 0;
|
||||
}
|
||||
|
||||
st.IsHooked = false;
|
||||
}
|
||||
|
||||
private static void TbOnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox tb)
|
||||
{
|
||||
Apply(tb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TbOnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox tb)
|
||||
{
|
||||
Detach(tb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TbOnGotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox tb)
|
||||
{
|
||||
Apply(tb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Apply(TextBox tb)
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyCore(tb);
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyCore(TextBox tb)
|
||||
{
|
||||
// Ensure template is realized
|
||||
tb.ApplyTemplate();
|
||||
|
||||
// Find the internal ScrollContentPresenter within the TextBox template
|
||||
var scp = tb.FindDescendant<ScrollContentPresenter>(s => s.Name == "ScrollContentPresenter");
|
||||
if (scp is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var brush = tb.Foreground; // use the actual current foreground brush
|
||||
if (brush == null)
|
||||
{
|
||||
brush = new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
|
||||
foreach (var rect in scp.FindDescendants().OfType<Rectangle>())
|
||||
{
|
||||
try
|
||||
{
|
||||
rect.Fill = brush;
|
||||
rect.CompositeMode = ElementCompositeMode.SourceOver;
|
||||
rect.Opacity = 0.9;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort; some rectangles might be template-owned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class State
|
||||
{
|
||||
public long ForegroundToken { get; set; }
|
||||
|
||||
public bool IsHooked { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using ManagedCommon;
|
||||
using ManagedCsWin32;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight helper to access wallpaper information.
|
||||
/// </summary>
|
||||
internal sealed partial class WallpaperHelper
|
||||
{
|
||||
private readonly IDesktopWallpaper? _desktopWallpaper;
|
||||
|
||||
public WallpaperHelper()
|
||||
{
|
||||
try
|
||||
{
|
||||
var desktopWallpaper = ComHelper.CreateComInstance<IDesktopWallpaper>(
|
||||
ref Unsafe.AsRef(in CLSID.DesktopWallpaper),
|
||||
CLSCTX.ALL);
|
||||
|
||||
_desktopWallpaper = desktopWallpaper;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If COM initialization fails, keep helper usable with safe fallbacks
|
||||
Logger.LogError("Failed to initialize DesktopWallpaper COM interface", ex);
|
||||
_desktopWallpaper = null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetWallpaperPathForFirstMonitor()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_desktopWallpaper is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_desktopWallpaper.GetMonitorDevicePathCount(out var monitorCount);
|
||||
|
||||
for (uint i = 0; monitorCount != 0 && i < monitorCount; i++)
|
||||
{
|
||||
_desktopWallpaper.GetMonitorDevicePathAt(i, out var monitorId);
|
||||
if (string.IsNullOrEmpty(monitorId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_desktopWallpaper.GetWallpaper(monitorId, out var wallpaperPath);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(wallpaperPath) && File.Exists(wallpaperPath))
|
||||
{
|
||||
return wallpaperPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to query wallpaper path", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wallpaper background color.
|
||||
/// </summary>
|
||||
/// <returns>The wallpaper background color, or black if it cannot be determined.</returns>
|
||||
public Color GetWallpaperColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_desktopWallpaper is null)
|
||||
{
|
||||
return Colors.Black;
|
||||
}
|
||||
|
||||
_desktopWallpaper.GetBackgroundColor(out var colorref);
|
||||
var r = (byte)(colorref.Value & 0x000000FF);
|
||||
var g = (byte)((colorref.Value & 0x0000FF00) >> 8);
|
||||
var b = (byte)((colorref.Value & 0x00FF0000) >> 16);
|
||||
return Color.FromArgb(255, r, g, b);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load wallpaper color", ex);
|
||||
return Colors.Black;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wallpaper image for the primary monitor.
|
||||
/// </summary>
|
||||
/// <returns>The wallpaper image, or null if it cannot be determined.</returns>
|
||||
public BitmapImage? GetWallpaperImage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = GetWallpaperPathForFirstMonitor();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var image = new BitmapImage();
|
||||
using var stream = File.OpenRead(path);
|
||||
var randomAccessStream = stream.AsRandomAccessStream();
|
||||
if (randomAccessStream == null)
|
||||
{
|
||||
Logger.LogError("Failed to convert file stream to RandomAccessStream for wallpaper image.");
|
||||
return null;
|
||||
}
|
||||
|
||||
image.SetSource(randomAccessStream);
|
||||
return image;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load wallpaper image", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// blittable type for COM interop
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal readonly partial struct COLORREF
|
||||
{
|
||||
internal readonly uint Value;
|
||||
}
|
||||
|
||||
// blittable type for COM interop
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal readonly partial struct RECT
|
||||
{
|
||||
internal readonly int Left;
|
||||
internal readonly int Top;
|
||||
internal readonly int Right;
|
||||
internal readonly int Bottom;
|
||||
}
|
||||
|
||||
// COM interface for IDesktopWallpaper, GeneratedComInterface to be AOT compatible
|
||||
[GeneratedComInterface]
|
||||
[Guid("B92B56A9-8B55-4E14-9A89-0199BBB6F93B")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal partial interface IDesktopWallpaper
|
||||
{
|
||||
void SetWallpaper(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? monitorId,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string wallpaper);
|
||||
|
||||
void GetWallpaper(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? monitorId,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] out string wallpaper);
|
||||
|
||||
void GetMonitorDevicePathAt(uint monitorIndex, [MarshalAs(UnmanagedType.LPWStr)] out string monitorId);
|
||||
|
||||
void GetMonitorDevicePathCount(out uint count);
|
||||
|
||||
void GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string? monitorId, out RECT rect);
|
||||
|
||||
void SetBackgroundColor(COLORREF color);
|
||||
|
||||
void GetBackgroundColor(out COLORREF color);
|
||||
|
||||
// Other methods omitted for brevity
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
x:Class="Microsoft.CmdPal.UI.MainWindow"
|
||||
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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="using:Microsoft.CmdPal.UI.Pages"
|
||||
@@ -15,6 +16,21 @@
|
||||
Closed="MainWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
<Grid x:Name="RootElement">
|
||||
|
||||
<controls:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind ViewModel.BackgroundImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
IsHoldingEnabled="False"
|
||||
TintColor="{x:Bind ViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<pages:ShellPage />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -15,8 +15,10 @@ using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI;
|
||||
@@ -66,7 +68,10 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private readonly KeyboardListener _keyboardListener;
|
||||
private readonly LocalKeyboardListener _localKeyboardListener;
|
||||
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
|
||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||
private bool _themeServiceInitialized;
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
@@ -74,13 +79,21 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
private MainWindowViewModel ViewModel { get; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ViewModel = App.Current.Services.GetService<MainWindowViewModel>()!;
|
||||
|
||||
_autoGoHomeTimer = new DispatcherTimer();
|
||||
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
|
||||
|
||||
_themeService = App.Current.Services.GetRequiredService<IThemeService>();
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_windowThemeSynchronizer = new WindowThemeSynchronizer(_themeService, this);
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
|
||||
unsafe
|
||||
@@ -88,6 +101,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
||||
}
|
||||
|
||||
SetAcrylic();
|
||||
|
||||
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
|
||||
|
||||
_keyboardListener = new KeyboardListener();
|
||||
@@ -100,8 +115,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
RestoreWindowPosition();
|
||||
UpdateWindowPositionInMemory();
|
||||
|
||||
SetAcrylic();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
||||
@@ -156,6 +169,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
|
||||
}
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
UpdateAcrylic();
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.GoBack)
|
||||
@@ -247,8 +265,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
||||
}
|
||||
|
||||
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
|
||||
// other Shell surfaces are using, this cannot be set in XAML however.
|
||||
private void SetAcrylic()
|
||||
{
|
||||
if (DesktopAcrylicController.IsSupported())
|
||||
@@ -265,41 +281,32 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void UpdateAcrylic()
|
||||
{
|
||||
if (_acrylicController != null)
|
||||
try
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
}
|
||||
|
||||
_acrylicController = GetAcrylicConfig(Content);
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private static DesktopAcrylicController GetAcrylicConfig(UIElement content)
|
||||
{
|
||||
var feContent = content as FrameworkElement;
|
||||
|
||||
return feContent?.ActualTheme == ElementTheme.Light
|
||||
? new DesktopAcrylicController()
|
||||
if (_acrylicController != null)
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = Color.FromArgb(255, 243, 243, 243),
|
||||
LuminosityOpacity = 0.90f,
|
||||
TintOpacity = 0.0f,
|
||||
FallbackColor = Color.FromArgb(255, 238, 238, 238),
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
}
|
||||
: new DesktopAcrylicController()
|
||||
|
||||
var backdrop = _themeService.Current.BackdropParameters;
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = Color.FromArgb(255, 32, 32, 32),
|
||||
LuminosityOpacity = 0.96f,
|
||||
TintOpacity = 0.5f,
|
||||
FallbackColor = Color.FromArgb(255, 28, 28, 28),
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = backdrop.TintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.LuminosityOpacity,
|
||||
};
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to update backdrop", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
@@ -711,6 +718,19 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
|
||||
{
|
||||
try
|
||||
{
|
||||
_themeService.Initialize();
|
||||
_themeServiceInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to initialize ThemeService", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
// Save the current window position before hiding the window
|
||||
@@ -1004,6 +1024,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
public void Dispose()
|
||||
{
|
||||
_localKeyboardListener.Dispose();
|
||||
_windowThemeSynchronizer.Dispose();
|
||||
DisposeAcrylic();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\ActionBar.xaml" />
|
||||
<None Remove="Controls\ColorPalette.xaml" />
|
||||
<None Remove="Controls\CommandPalettePreview.xaml" />
|
||||
<None Remove="Controls\DevRibbon.xaml" />
|
||||
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="Controls\ScreenPreview.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="IsEnabledTextBlock.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
@@ -78,10 +81,12 @@
|
||||
<None Remove="Pages\Settings\ExtensionsPage.xaml" />
|
||||
<None Remove="Pages\Settings\GeneralPage.xaml" />
|
||||
<None Remove="SettingsWindow.xaml" />
|
||||
<None Remove="Settings\AppearancePage.xaml" />
|
||||
<None Remove="ShellPage.xaml" />
|
||||
<None Remove="Styles\Colors.xaml" />
|
||||
<None Remove="Styles\Settings.xaml" />
|
||||
<None Remove="Styles\TextBox.xaml" />
|
||||
<None Remove="Styles\Theme.Normal.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -93,6 +98,7 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||
@@ -207,6 +213,39 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\CommandPalettePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ScreenPreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ColorPalette.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Theme.Colorful.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Styles\Theme.Normal.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Settings\AppearancePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="IsEnabledTextBlock.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
|
||||
xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
@@ -27,6 +26,7 @@
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<cmdpalUI:DetailsSizeToGridLengthConverter x:Key="SizeToWidthConverter" />
|
||||
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
|
||||
|
||||
<cmdpalUI:DetailsDataTemplateSelector
|
||||
@@ -177,7 +177,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
|
||||
<Grid Grid.Row="0" Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -190,7 +190,7 @@
|
||||
Padding="0,12,0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -371,7 +371,7 @@
|
||||
<Grid x:Name="ContentGrid" Grid.Row="1">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="3*" />
|
||||
<ColumnDefinition Width="{x:Bind ViewModel.Details.Size, Mode=OneWay, Converter={StaticResource SizeToWidthConverter}}" />
|
||||
<ColumnDefinition x:Name="DetailsColumn" Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CmdPal.DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="Collapsed">
|
||||
@@ -518,7 +518,7 @@
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CmdPal.DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<cpcontrols:CommandBar CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// 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 Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides theme appropriate for colorful (accented) appearance.
|
||||
/// </summary>
|
||||
internal sealed class ColorfulThemeProvider : IThemeProvider
|
||||
{
|
||||
// Fluent dark: #202020
|
||||
private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32);
|
||||
|
||||
// Fluent light: #F3F3F3
|
||||
private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243);
|
||||
|
||||
private readonly UISettings _uiSettings;
|
||||
|
||||
public string ThemeKey => "colorful";
|
||||
|
||||
public string ResourcePath => "ms-appx:///Styles/Theme.Colorful.xaml";
|
||||
|
||||
public ColorfulThemeProvider(UISettings uiSettings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(uiSettings);
|
||||
_uiSettings = uiSettings;
|
||||
}
|
||||
|
||||
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
|
||||
{
|
||||
var isLight = context.Theme == ElementTheme.Light ||
|
||||
(context.Theme == ElementTheme.Default &&
|
||||
_uiSettings.GetColorValue(UIColorType.Background).R > 128);
|
||||
|
||||
var baseColor = isLight ? LightBaseColor : DarkBaseColor;
|
||||
|
||||
// Windows is warping the hue of accent colors and running it through some curves to produce their accent shades.
|
||||
// This will attempt to mimic that behavior.
|
||||
var accentShades = AccentShades.Compute(context.Tint.LerpHsv(WindowsAccentHueWarpTransform.Transform(context.Tint), 0.5f));
|
||||
var blended = isLight ? accentShades.Light3 : accentShades.Dark2;
|
||||
var colorIntensityUser = (context.ColorIntensity ?? 100) / 100f;
|
||||
|
||||
// For light theme, we want to reduce intensity a bit, and also we need to keep the color fairly light,
|
||||
// to avoid issues with text box caret.
|
||||
var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser;
|
||||
var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity);
|
||||
|
||||
return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f);
|
||||
}
|
||||
|
||||
private static class ColorBlender
|
||||
{
|
||||
/// <summary>
|
||||
/// Blends a semitransparent tint color over an opaque base color using alpha compositing.
|
||||
/// </summary>
|
||||
/// <param name="baseColor">The opaque base color (background)</param>
|
||||
/// <param name="tintColor">The semitransparent tint color (foreground)</param>
|
||||
/// <param name="intensity">The intensity of the tint (0.0 - 1.0)</param>
|
||||
/// <returns>The resulting blended color</returns>
|
||||
public static Color Blend(Color baseColor, Color tintColor, float intensity)
|
||||
{
|
||||
// Normalize alpha to 0.0 - 1.0 range
|
||||
intensity = Math.Clamp(intensity, 0f, 1f);
|
||||
|
||||
// Alpha compositing formula: result = tint * alpha + base * (1 - alpha)
|
||||
var r = (byte)((tintColor.R * intensity) + (baseColor.R * (1 - intensity)));
|
||||
var g = (byte)((tintColor.G * intensity) + (baseColor.G * (1 - intensity)));
|
||||
var b = (byte)((tintColor.B * intensity) + (baseColor.B * (1 - intensity)));
|
||||
|
||||
// Result is fully opaque since base is opaque
|
||||
return Color.FromArgb(255, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
private static class WindowsAccentHueWarpTransform
|
||||
{
|
||||
private static readonly (double HIn, double HOut)[] HueMap =
|
||||
[
|
||||
(0, 0),
|
||||
(10, 1),
|
||||
(20, 6),
|
||||
(30, 10),
|
||||
(40, 14),
|
||||
(50, 19),
|
||||
(60, 36),
|
||||
(70, 94),
|
||||
(80, 112),
|
||||
(90, 120),
|
||||
(100, 120),
|
||||
(110, 120),
|
||||
(120, 120),
|
||||
(130, 120),
|
||||
(140, 120),
|
||||
(150, 125),
|
||||
(160, 135),
|
||||
(170, 142),
|
||||
(180, 178),
|
||||
(190, 205),
|
||||
(200, 220),
|
||||
(210, 229),
|
||||
(220, 237),
|
||||
(230, 241),
|
||||
(240, 243),
|
||||
(250, 244),
|
||||
(260, 245),
|
||||
(270, 248),
|
||||
(280, 252),
|
||||
(290, 276),
|
||||
(300, 293),
|
||||
(310, 313),
|
||||
(320, 330),
|
||||
(330, 349),
|
||||
(340, 353),
|
||||
(350, 357)
|
||||
];
|
||||
|
||||
public static Color Transform(Color input, Options? opt = null)
|
||||
{
|
||||
opt ??= new Options();
|
||||
var hsv = input.ToHsv();
|
||||
return ColorHelper.FromHsv(
|
||||
RemapHueLut(hsv.H),
|
||||
Clamp01(Math.Pow(hsv.S, opt.SaturationGamma) * opt.SaturationGain),
|
||||
Clamp01((opt.ValueScaleA * hsv.V) + opt.ValueBiasB),
|
||||
input.A);
|
||||
}
|
||||
|
||||
// Hue LUT remap (piecewise-linear with cyclic wrap)
|
||||
private static double RemapHueLut(double hDeg)
|
||||
{
|
||||
// Normalize to [0,360)
|
||||
hDeg = Mod(hDeg, 360.0);
|
||||
|
||||
// Handle wrap-around case: hDeg is between last entry (350°) and 360°
|
||||
var last = HueMap[^1];
|
||||
var first = HueMap[0];
|
||||
if (hDeg >= last.HIn)
|
||||
{
|
||||
// Interpolate between last entry and first entry (wrapped by 360°)
|
||||
var t = (hDeg - last.HIn) / (first.HIn + 360.0 - last.HIn + 1e-12);
|
||||
var ho = Lerp(last.HOut, first.HOut + 360.0, t);
|
||||
return Mod(ho, 360.0);
|
||||
}
|
||||
|
||||
// Find segment [i, i+1] where HueMap[i].HIn <= hDeg < HueMap[i+1].HIn
|
||||
for (var i = 0; i < HueMap.Length - 1; i++)
|
||||
{
|
||||
var a = HueMap[i];
|
||||
var b = HueMap[i + 1];
|
||||
|
||||
if (hDeg >= a.HIn && hDeg < b.HIn)
|
||||
{
|
||||
var t = (hDeg - a.HIn) / (b.HIn - a.HIn + 1e-12);
|
||||
return Lerp(a.HOut, b.HOut, t);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback (shouldn't happen)
|
||||
return hDeg;
|
||||
}
|
||||
|
||||
private static double Lerp(double a, double b, double t) => a + ((b - a) * t);
|
||||
|
||||
private static double Mod(double x, double m) => ((x % m) + m) % m;
|
||||
|
||||
private static double Clamp01(double x) => x < 0 ? 0 : (x > 1 ? 1 : x);
|
||||
|
||||
public sealed class Options
|
||||
{
|
||||
// Saturation boost (1.0 = no change). Typical: 1.3–1.8
|
||||
public double SaturationGain { get; init; } = 1.0;
|
||||
|
||||
// Optional saturation gamma (1.0 = linear). <1.0 raises low S a bit; >1.0 preserves low S.
|
||||
public double SaturationGamma { get; init; } = 1.0;
|
||||
|
||||
// Value (V) remap: V' = a*V + b (tone curve; clamp applied)
|
||||
// Example that lifts blacks & compresses whites slightly: a=0.50, b=0.08
|
||||
public double ValueScaleA { get; init; } = 0.6;
|
||||
|
||||
public double ValueBiasB { get; init; } = 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AccentShades
|
||||
{
|
||||
public static (Color Light3, Color Light2, Color Light1, Color Dark1, Color Dark2, Color Dark3) Compute(Color accent)
|
||||
{
|
||||
var light1 = accent.Update(brightnessFactor: 0.15, saturationFactor: -0.12);
|
||||
var light2 = accent.Update(brightnessFactor: 0.30, saturationFactor: -0.24);
|
||||
var light3 = accent.Update(brightnessFactor: 0.45, saturationFactor: -0.36);
|
||||
|
||||
var dark1 = accent.UpdateBrightness(brightnessFactor: -0.05f);
|
||||
var dark2 = accent.UpdateBrightness(brightnessFactor: -0.01f);
|
||||
var dark3 = accent.UpdateBrightness(brightnessFactor: -0.015f);
|
||||
|
||||
return (light3, light2, light1, dark1, dark2, dark3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides theme identification, resource path resolution, and creation of acrylic
|
||||
/// backdrop parameters based on the current <see cref="ThemeContext"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations should expose a stable <see cref="ThemeKey"/> and a valid XAML resource
|
||||
/// dictionary path via <see cref="ResourcePath"/>. The
|
||||
/// <see cref="GetAcrylicBackdrop(ThemeContext)"/> method computes
|
||||
/// <see cref="AcrylicBackdropParameters"/> using the supplied theme context.
|
||||
/// </remarks>
|
||||
internal interface IThemeProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique key identifying this theme provider.
|
||||
/// </summary>
|
||||
string ThemeKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resource dictionary path for this theme.
|
||||
/// </summary>
|
||||
string ResourcePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates acrylic backdrop parameters based on the provided theme context.
|
||||
/// </summary>
|
||||
/// <param name="context">The current theme context, including theme, tint, and optional background details.</param>
|
||||
/// <returns>The computed <see cref="AcrylicBackdropParameters"/> for the backdrop.</returns>
|
||||
AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated ResourceDictionary for dynamic overrides that win over base theme resources. Since
|
||||
/// we can't use a key or name to identify the dictionary in Application resources, we use a dedicated type.
|
||||
/// </summary>
|
||||
internal sealed partial class MutableOverridesDictionary : ResourceDictionary;
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides theme resources and acrylic backdrop parameters matching the default Command Palette theme.
|
||||
/// </summary>
|
||||
internal sealed class NormalThemeProvider : IThemeProvider
|
||||
{
|
||||
private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32);
|
||||
private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243);
|
||||
private readonly UISettings _uiSettings;
|
||||
|
||||
public NormalThemeProvider(UISettings uiSettings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(uiSettings);
|
||||
_uiSettings = uiSettings;
|
||||
}
|
||||
|
||||
public string ThemeKey => "normal";
|
||||
|
||||
public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml";
|
||||
|
||||
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
|
||||
{
|
||||
var isLight = context.Theme == ElementTheme.Light ||
|
||||
(context.Theme == ElementTheme.Default &&
|
||||
_uiSettings.GetColorValue(UIColorType.Background).R > 128);
|
||||
|
||||
return new AcrylicBackdropParameters(
|
||||
TintColor: isLight ? LightBaseColor : DarkBaseColor,
|
||||
FallbackColor: isLight ? LightBaseColor : DarkBaseColor,
|
||||
TintOpacity: 0.5f,
|
||||
LuminosityOpacity: isLight ? 0.9f : 0.96f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Simple theme switcher that swaps application ResourceDictionaries at runtime.
|
||||
/// Can also operate in event-only mode for consumers to apply resources themselves.
|
||||
/// Exposes a dedicated override dictionary that stays merged and is cleared on theme changes.
|
||||
/// </summary>
|
||||
internal sealed partial class ResourceSwapper
|
||||
{
|
||||
private readonly Lock _resourceSwapGate = new();
|
||||
private readonly Dictionary<string, Uri> _themeUris = new(StringComparer.OrdinalIgnoreCase);
|
||||
private ResourceDictionary? _activeDictionary;
|
||||
private string? _currentThemeName;
|
||||
private Uri? _currentThemeUri;
|
||||
|
||||
private ResourceDictionary? _overrideDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a theme has been activated.
|
||||
/// </summary>
|
||||
public event EventHandler<ResourcesSwappedEventArgs>? ResourcesSwapped;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether when true (default) ResourceSwapper updates Application.Current.Resources. When false, it only raises ResourcesSwapped.
|
||||
/// </summary>
|
||||
public bool ApplyToAppResources { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets name of the currently selected theme (if any).
|
||||
/// </summary>
|
||||
public string? CurrentThemeName
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
return _currentThemeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes ResourceSwapper by checking Application resources for an already merged theme dictionary.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
// Find merged dictionary in Application resources that matches a registered theme by URI
|
||||
// This allows ResourceSwapper to pick up an initial theme set in XAML
|
||||
var app = Application.Current;
|
||||
var resourcesMergedDictionaries = app?.Resources?.MergedDictionaries;
|
||||
if (resourcesMergedDictionaries == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var dict in resourcesMergedDictionaries)
|
||||
{
|
||||
var uri = dict.Source;
|
||||
if (uri is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = GetNameForUri(uri);
|
||||
if (name is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
_currentThemeName = name;
|
||||
_currentThemeUri = uri;
|
||||
_activeDictionary = dict;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets uri of the currently selected theme dictionary (if any).
|
||||
/// </summary>
|
||||
public Uri? CurrentThemeUri
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
return _currentThemeUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ResourceDictionary GetOverrideDictionary(bool clear = false)
|
||||
{
|
||||
var app = Application.Current ?? throw new InvalidOperationException("App is null");
|
||||
|
||||
if (app.Resources == null)
|
||||
{
|
||||
throw new InvalidOperationException("Application.Resources is null");
|
||||
}
|
||||
|
||||
// (Re)locate the slot – Hot Reload may rebuild Application.Resources.
|
||||
var slot = app.Resources!.MergedDictionaries!
|
||||
.OfType<MutableOverridesDictionary>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (slot is null)
|
||||
{
|
||||
// If the slot vanished (Hot Reload), create it again at the end so it wins precedence.
|
||||
slot = new MutableOverridesDictionary();
|
||||
app.Resources.MergedDictionaries!.Add(slot);
|
||||
}
|
||||
|
||||
// Ensure the slot has exactly one child RD we can swap safely.
|
||||
if (slot.MergedDictionaries!.Count == 0)
|
||||
{
|
||||
slot.MergedDictionaries.Add(new ResourceDictionary());
|
||||
}
|
||||
else if (slot.MergedDictionaries.Count > 1)
|
||||
{
|
||||
// Normalize to a single child to keep semantics predictable.
|
||||
var keep = slot.MergedDictionaries[^1];
|
||||
slot.MergedDictionaries.Clear();
|
||||
slot.MergedDictionaries.Add(keep);
|
||||
}
|
||||
|
||||
if (clear)
|
||||
{
|
||||
// Swap the child dictionary instead of Clear() to avoid reentrancy issues.
|
||||
var fresh = new ResourceDictionary();
|
||||
slot.MergedDictionaries[0] = fresh;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
return slot.MergedDictionaries[0]!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a theme name mapped to a XAML ResourceDictionary URI (e.g. ms-appx:///Themes/Red.xaml)
|
||||
/// </summary>
|
||||
public void RegisterTheme(string name, Uri dictionaryUri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Theme name is required", nameof(name));
|
||||
}
|
||||
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
_themeUris[name] = dictionaryUri ?? throw new ArgumentNullException(nameof(dictionaryUri));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a theme with a string URI.
|
||||
/// </summary>
|
||||
public void RegisterTheme(string name, string dictionaryUri)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dictionaryUri);
|
||||
RegisterTheme(name, new Uri(dictionaryUri));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered theme.
|
||||
/// </summary>
|
||||
public bool UnregisterTheme(string name)
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
return _themeUris.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the names of all registered themes.
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetRegisteredThemes()
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
// return a copy to avoid external mutation
|
||||
return new List<string>(_themeUris.Keys);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates a theme by name. The dictionary for the given name must be registered first.
|
||||
/// </summary>
|
||||
public void ActivateTheme(string theme)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(theme))
|
||||
{
|
||||
throw new ArgumentException("Theme name is required", nameof(theme));
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
if (!_themeUris.TryGetValue(theme, out uri!))
|
||||
{
|
||||
throw new KeyNotFoundException($"Theme '{theme}' is not registered.");
|
||||
}
|
||||
}
|
||||
|
||||
ActivateThemeInternal(theme, uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to activate a theme by name without throwing.
|
||||
/// </summary>
|
||||
public bool TryActivateTheme(string theme)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(theme))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
if (!_themeUris.TryGetValue(theme, out uri!))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ActivateThemeInternal(theme, uri);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates a theme by URI to a ResourceDictionary.
|
||||
/// </summary>
|
||||
public void ActivateTheme(Uri dictionaryUri)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dictionaryUri);
|
||||
|
||||
ActivateThemeInternal(GetNameForUri(dictionaryUri), dictionaryUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the currently active theme ResourceDictionary. Also clears the override dictionary.
|
||||
/// </summary>
|
||||
public void ClearActiveTheme()
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeDictionary is not null && ApplyToAppResources)
|
||||
{
|
||||
_ = app.Resources.MergedDictionaries.Remove(_activeDictionary);
|
||||
_activeDictionary = null;
|
||||
}
|
||||
|
||||
// Clear overrides but keep the override dictionary merged for future updates
|
||||
_overrideDictionary?.Clear();
|
||||
|
||||
_currentThemeName = null;
|
||||
_currentThemeUri = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateThemeInternal(string? name, Uri dictionaryUri)
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
_currentThemeName = name;
|
||||
_currentThemeUri = dictionaryUri;
|
||||
}
|
||||
|
||||
if (ApplyToAppResources)
|
||||
{
|
||||
ActivateThemeCore(dictionaryUri);
|
||||
}
|
||||
|
||||
OnResourcesSwapped(new(name, dictionaryUri));
|
||||
}
|
||||
|
||||
private void ActivateThemeCore(Uri dictionaryUri)
|
||||
{
|
||||
var app = Application.Current ?? throw new InvalidOperationException("Application.Current is null");
|
||||
|
||||
// Remove previously applied base theme dictionary
|
||||
if (_activeDictionary is not null)
|
||||
{
|
||||
_ = app.Resources.MergedDictionaries.Remove(_activeDictionary);
|
||||
_activeDictionary = null;
|
||||
}
|
||||
|
||||
// Load and merge the new base theme dictionary
|
||||
var newDict = new ResourceDictionary { Source = dictionaryUri };
|
||||
app.Resources.MergedDictionaries.Add(newDict);
|
||||
_activeDictionary = newDict;
|
||||
|
||||
// Ensure override dictionary exists and is merged last, then clear it to avoid leaking stale overrides
|
||||
_overrideDictionary = GetOverrideDictionary(clear: true);
|
||||
}
|
||||
|
||||
private string? GetNameForUri(Uri dictionaryUri)
|
||||
{
|
||||
lock (_resourceSwapGate)
|
||||
{
|
||||
foreach (var (key, value) in _themeUris)
|
||||
{
|
||||
if (Uri.Compare(value, dictionaryUri, UriComponents.AbsoluteUri, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResourcesSwapped(ResourcesSwappedEventArgs e)
|
||||
{
|
||||
ResourcesSwapped?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
@@ -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.Services;
|
||||
|
||||
public sealed class ResourcesSwappedEventArgs(string? name, Uri dictionaryUri) : EventArgs
|
||||
{
|
||||
public string? Name { get; } = name;
|
||||
|
||||
public Uri DictionaryUri { get; } = dictionaryUri ?? throw new ArgumentNullException(nameof(dictionaryUri));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Services;
|
||||
|
||||
internal sealed record ThemeContext
|
||||
{
|
||||
public ElementTheme Theme { get; init; }
|
||||
|
||||
public Color Tint { get; init; }
|
||||
|
||||
public ImageSource? BackgroundImageSource { get; init; }
|
||||
|
||||
public Stretch BackgroundImageStretch { get; init; }
|
||||
|
||||
public double BackgroundImageOpacity { get; init; }
|
||||
|
||||
public int? ColorIntensity { get; init; }
|
||||
}
|
||||
261
src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs
Normal file
261
src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.UI.ViewManagement;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// ThemeService is a hub that translates user settings and system preferences into concrete
|
||||
/// theme resources and notifies listeners of changes.
|
||||
/// </summary>
|
||||
internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan ReloadDebounceInterval = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ResourceSwapper _resourceSwapper;
|
||||
private readonly NormalThemeProvider _normalThemeProvider;
|
||||
private readonly ColorfulThemeProvider _colorfulThemeProvider;
|
||||
|
||||
private DispatcherQueue? _dispatcherQueue;
|
||||
private DispatcherQueueTimer? _dispatcherQueueTimer;
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
private InternalThemeState _currentState;
|
||||
|
||||
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||
|
||||
public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the theme service. Must be called after the application window is activated and on UI thread.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
if (_dispatcherQueue is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to get DispatcherQueue for the current thread. Ensure Initialize is called on the UI thread after window activation.");
|
||||
}
|
||||
|
||||
_dispatcherQueueTimer = _dispatcherQueue.CreateTimer();
|
||||
|
||||
_resourceSwapper.Initialize();
|
||||
_isInitialized = true;
|
||||
Reload();
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// provider selection
|
||||
var intensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
|
||||
IThemeProvider provider = intensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
|
||||
? _colorfulThemeProvider
|
||||
: _normalThemeProvider;
|
||||
|
||||
// Calculate values
|
||||
var tint = _settings.ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.CustomColor => _settings.CustomThemeColor,
|
||||
ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
|
||||
ColorizationMode.Image => _settings.CustomThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
var effectiveTheme = GetElementTheme((ElementTheme)_settings.Theme);
|
||||
var imageSource = _settings.ColorizationMode == ColorizationMode.Image
|
||||
? LoadImageSafe(_settings.BackgroundImagePath)
|
||||
: null;
|
||||
var stretch = _settings.BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => Stretch.Fill,
|
||||
_ => Stretch.UniformToFill,
|
||||
};
|
||||
var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
|
||||
|
||||
// create context and offload to actual theme provider
|
||||
var context = new ThemeContext
|
||||
{
|
||||
Tint = tint,
|
||||
ColorIntensity = intensity,
|
||||
Theme = effectiveTheme,
|
||||
BackgroundImageSource = imageSource,
|
||||
BackgroundImageStretch = stretch,
|
||||
BackgroundImageOpacity = opacity,
|
||||
};
|
||||
var backdrop = provider.GetAcrylicBackdrop(context);
|
||||
var blur = _settings.BackgroundImageBlurAmount;
|
||||
var brightness = _settings.BackgroundImageBrightness;
|
||||
|
||||
// Create public snapshot (no provider!)
|
||||
var snapshot = new ThemeSnapshot
|
||||
{
|
||||
Tint = tint,
|
||||
TintIntensity = intensity / 100f,
|
||||
Theme = effectiveTheme,
|
||||
BackgroundImageSource = imageSource,
|
||||
BackgroundImageStretch = stretch,
|
||||
BackgroundImageOpacity = opacity,
|
||||
BackdropParameters = backdrop,
|
||||
BlurAmount = blur,
|
||||
BackgroundBrightness = brightness / 100f,
|
||||
};
|
||||
|
||||
// Bundle with provider for internal use
|
||||
var newState = new InternalThemeState
|
||||
{
|
||||
Snapshot = snapshot,
|
||||
Provider = provider,
|
||||
};
|
||||
|
||||
// Atomic swap
|
||||
Interlocked.Exchange(ref _currentState, newState);
|
||||
|
||||
_resourceSwapper.TryActivateTheme(provider.ThemeKey);
|
||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
|
||||
}
|
||||
|
||||
private static BitmapImage? LoadImageSafe(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If it looks like a file path and exists, prefer absolute file URI
|
||||
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!uri.IsAbsoluteUri && File.Exists(path))
|
||||
{
|
||||
uri = new Uri(Path.GetFullPath(path));
|
||||
}
|
||||
|
||||
return new BitmapImage(uri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to load background image '{path}'. {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ThemeService(SettingsModel settings, ResourceSwapper resourceSwapper)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
ArgumentNullException.ThrowIfNull(resourceSwapper);
|
||||
|
||||
_settings = settings;
|
||||
_settings.SettingsChanged += SettingsOnSettingsChanged;
|
||||
|
||||
_resourceSwapper = resourceSwapper;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettings_ColorValuesChanged;
|
||||
|
||||
_normalThemeProvider = new NormalThemeProvider(_uiSettings);
|
||||
_colorfulThemeProvider = new ColorfulThemeProvider(_uiSettings);
|
||||
List<IThemeProvider> providers = [_normalThemeProvider, _colorfulThemeProvider];
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
_resourceSwapper.RegisterTheme(provider.ThemeKey, provider.ResourcePath);
|
||||
}
|
||||
|
||||
_currentState = new InternalThemeState
|
||||
{
|
||||
Snapshot = new ThemeSnapshot
|
||||
{
|
||||
Tint = Colors.Transparent,
|
||||
Theme = ElementTheme.Light,
|
||||
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
|
||||
BackgroundImageOpacity = 1,
|
||||
BackgroundImageSource = null,
|
||||
BackgroundImageStretch = Stretch.Fill,
|
||||
BlurAmount = 0,
|
||||
TintIntensity = 1.0f,
|
||||
BackgroundBrightness = 0,
|
||||
},
|
||||
Provider = _normalThemeProvider,
|
||||
};
|
||||
}
|
||||
|
||||
private void RequestReload()
|
||||
{
|
||||
if (!_isInitialized || _dispatcherQueueTimer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcherQueueTimer.Debounce(Reload, ReloadDebounceInterval);
|
||||
}
|
||||
|
||||
private ElementTheme GetElementTheme(ElementTheme theme)
|
||||
{
|
||||
return theme switch
|
||||
{
|
||||
ElementTheme.Light => ElementTheme.Light,
|
||||
ElementTheme.Dark => ElementTheme.Dark,
|
||||
_ => _uiSettings.GetColorValue(UIColorType.Background).CalculateBrightness() < 0.5
|
||||
? ElementTheme.Dark
|
||||
: ElementTheme.Light,
|
||||
};
|
||||
}
|
||||
|
||||
private void SettingsOnSettingsChanged(SettingsModel sender, object? args)
|
||||
{
|
||||
RequestReload();
|
||||
}
|
||||
|
||||
private void UiSettings_ColorValuesChanged(UISettings sender, object args)
|
||||
{
|
||||
RequestReload();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_dispatcherQueueTimer?.Stop();
|
||||
_uiSettings.ColorValuesChanged -= UiSettings_ColorValuesChanged;
|
||||
_settings.SettingsChanged -= SettingsOnSettingsChanged;
|
||||
}
|
||||
|
||||
private sealed class InternalThemeState
|
||||
{
|
||||
public required ThemeSnapshot Snapshot { get; init; }
|
||||
|
||||
public required IThemeProvider Provider { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizes a window's theme with <see cref="IThemeService"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class WindowThemeSynchronizer : IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly Window _window;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WindowThemeSynchronizer"/> class and subscribes to theme changes.
|
||||
/// </summary>
|
||||
/// <param name="themeService">The theme service to monitor for changes.</param>
|
||||
/// <param name="window">The window to synchronize.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="themeService"/> or <paramref name="window"/> is null.</exception>
|
||||
public WindowThemeSynchronizer(IThemeService themeService, Window window)
|
||||
{
|
||||
_themeService = themeService ?? throw new ArgumentNullException(nameof(themeService));
|
||||
_window = window ?? throw new ArgumentNullException(nameof(window));
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from theme change events.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the current theme to the window when theme changes occur.
|
||||
/// </summary>
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
if (_window.Content is not FrameworkElement fe)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dispatcherQueue = fe.DispatcherQueue;
|
||||
|
||||
if (dispatcherQueue is not null && dispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
ApplyRequestedTheme(fe);
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcherQueue?.TryEnqueue(() => ApplyRequestedTheme(fe));
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRequestedTheme(FrameworkElement fe)
|
||||
{
|
||||
// LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
|
||||
// a refresh of the theme.
|
||||
fe.RequestedTheme = ElementTheme.Dark;
|
||||
fe.RequestedTheme = ElementTheme.Light;
|
||||
fe.RequestedTheme = _themeService.Current.Theme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.AppearancePage"
|
||||
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:helpers="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ptControls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<ptControls:ScreenPreview Margin="0,0,0,16" HorizontalAlignment="Left">
|
||||
<ptControls:CommandPalettePreview
|
||||
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
|
||||
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
|
||||
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
|
||||
PreviewBackgroundImageFit="{x:Bind ViewModel.Appearance.BackgroundImageFit, Mode=OneWay}"
|
||||
PreviewBackgroundImageSource="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageSource, Mode=OneWay}"
|
||||
PreviewBackgroundImageTint="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}"
|
||||
PreviewBackgroundImageTintIntensity="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=OneWay}"
|
||||
PreviewBackgroundOpacity="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintOpacity, Mode=OneWay}"
|
||||
RequestedTheme="{x:Bind ViewModel.Appearance.EffectiveTheme, Mode=OneWay}" />
|
||||
</ptControls:ScreenPreview>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_System" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Light_Automation" Tag="Light">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Light" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark_Automation" Tag="Dark">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="Settings_GeneralPage_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.Appearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||
<ComboBox
|
||||
x:Uid="Settings_GeneralPage_ColorizationMode"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- none -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="Settings_GeneralPage_NoBackground_SettingsCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsNoBackgroundVisible, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="Settings_GeneralPage_NoBackground_DescriptionTextBlock"
|
||||
Margin="24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- system accent color -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_WindowsAccentColor_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsAccentColorControlsVisible, Mode=OneWay}">
|
||||
<controls:SettingsCard.Description>
|
||||
<TextBlock>
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1" />
|
||||
<Hyperlink
|
||||
Click="OpenWindowsColorsSettings_Click"
|
||||
TextDecorations="None"
|
||||
UnderlineStyle="None">
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_OpenWindowsColorsLinkText" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Description>
|
||||
<controls:SettingsCard.Content>
|
||||
<Border
|
||||
MinWidth="32"
|
||||
MinHeight="32"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- background -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="Settings_GeneralPage_BackgroundImage_SettingsCard"
|
||||
Description="{x:Bind ViewModel.Appearance.BackgroundImagePath, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.BackgroundImageBrightness, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="50"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.Appearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background tint color and intensity -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintVisible, Mode=OneWay}">
|
||||
<ptControls:ColorPickerButton
|
||||
HasSelectedColor="True"
|
||||
IsAlphaEnabled="False"
|
||||
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
|
||||
SelectedColor="{x:Bind ViewModel.Appearance.ThemeColor, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Reset background image properties -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- 'Behavior' section -->
|
||||
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowAppDetails_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowAppDetails, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackspaceGoesBack_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.BackspaceGoesBack, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EscapeKeyBehavior_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.EscapeKeyBehaviorIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysGoBack" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysDismiss" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysHide" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_SingleClickActivation_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SingleClickActivates, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_DisableAnimations_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.DisableAnimations, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.Windows.Storage.Pickers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class AppearancePage : Page
|
||||
{
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
internal SettingsViewModel ViewModel { get; }
|
||||
|
||||
public AppearancePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
ViewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler);
|
||||
}
|
||||
|
||||
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (XamlRoot?.ContentIslandEnvironment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var windowId = XamlRoot?.ContentIslandEnvironment?.AppWindowId ?? new WindowId(0);
|
||||
|
||||
var picker = new FileOpenPicker(windowId)
|
||||
{
|
||||
CommitButtonText = ViewModels.Properties.Resources.builtin_settings_appearance_pick_background_image_title!,
|
||||
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
|
||||
ViewMode = PickerViewMode.Thumbnail,
|
||||
};
|
||||
|
||||
string[] extensions = [".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp", ".jxr"];
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
picker.FileTypeFilter!.Add(ext);
|
||||
}
|
||||
|
||||
var file = await picker.PickSingleFileAsync()!;
|
||||
if (file != null)
|
||||
{
|
||||
ViewModel.Appearance.BackgroundImagePath = file.Path ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to pick background image file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenWindowsColorsSettings_Click(Hyperlink sender, HyperlinkClickEventArgs args)
|
||||
{
|
||||
// LOAD BEARING (or BEAR LOADING?): Process.Start with UseShellExecute inside a XAML input event can trigger WinUI reentrancy
|
||||
// and cause FailFast crashes. Task.Run moves the call off the UI thread to prevent hard process termination.
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = Process.Start(new ProcessStartInfo("ms-settings:colors") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open Windows Settings", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -81,35 +81,10 @@
|
||||
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowAppDetails_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.ShowAppDetails, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackspaceGoesBack_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.BackspaceGoesBack, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EscapeKeyBehavior_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.EscapeKeyBehaviorIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysGoBack" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysDismiss" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysHide" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_SingleClickActivation_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.SingleClickActivates, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_DisableAnimations_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.DisableAnimations, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'For Developers' section -->
|
||||
|
||||
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_General"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="General" />
|
||||
<NavigationViewItem
|
||||
x:Name="AppearancePageNavItem"
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Appearance"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Appearance" />
|
||||
<NavigationViewItem
|
||||
x:Name="ExtensionPageNavItem"
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
|
||||
|
||||
@@ -101,6 +101,7 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
var pageType = page switch
|
||||
{
|
||||
"General" => typeof(GeneralPage),
|
||||
"Appearance" => typeof(AppearancePage),
|
||||
"Extensions" => typeof(ExtensionsPage),
|
||||
_ => null,
|
||||
};
|
||||
@@ -248,6 +249,12 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
var pageType = RS_.GetString("Settings_PageTitles_GeneralPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(AppearancePage))
|
||||
{
|
||||
NavView.SelectedItem = AppearancePageNavItem;
|
||||
var pageType = RS_.GetString("Settings_PageTitles_AppearancePage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(ExtensionsPage))
|
||||
{
|
||||
NavView.SelectedItem = ExtensionPageNavItem;
|
||||
|
||||
@@ -550,9 +550,69 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Automatically returns to home page after a period of inactivity when Command Palette is closed</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Appearance.Content" xml:space="preserve">
|
||||
<value>Personalization</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_SettingsCard.Header" xml:space="preserve">
|
||||
<value>App theme mode</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Select which app theme to display</value>
|
||||
</data>
|
||||
<data name="AppearanceSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_Mode_System.Text" xml:space="preserve">
|
||||
<value>Use system settings</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_Mode_Light.Text" xml:space="preserve">
|
||||
<value>Light</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_Mode_Dark.Text" xml:space="preserve">
|
||||
<value>Dark</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundTint_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color tint</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color intensity</value>
|
||||
</data>
|
||||
<data name="OptionalColorPickerButton_UnsetTextBlock.Text" xml:space="preserve">
|
||||
<value>Choose color</value>
|
||||
</data>
|
||||
<data name="OptionalColorPickerButton_ResetButton.Content" xml:space="preserve">
|
||||
<value>Use default</value>
|
||||
</data>
|
||||
<data name="OptionalColorPickerButton_TransparentColorButton.Content" xml:space="preserve">
|
||||
<value>Use default color</value>
|
||||
</data>
|
||||
<data name="OptionalColorPickerButton_WindowsColorsSectionHeading.Text" xml:space="preserve">
|
||||
<value>Windows colors</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImage_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImageOpacity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image opacity</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImageFit_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image fit</value>
|
||||
</data>
|
||||
<data name="BackgroundImageFit_ComboBoxItem_Fill.Content" xml:space="preserve">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="BackgroundImageFit_ComboBoxItem_Fit.Content" xml:space="preserve">
|
||||
<value>Fit</value>
|
||||
</data>
|
||||
<data name="BackgroundImageFit_ComboBoxItem_Stretch.Content" xml:space="preserve">
|
||||
<value>Stretch</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_GeneralPage" xml:space="preserve">
|
||||
<value>General</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_AppearancePage" xml:space="preserve">
|
||||
<value>Personalization</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
@@ -577,4 +637,73 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="SettingsButtonTextBlock.Text" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="OptionalColorPickerButton_CustomColorsSectionHeading.Text" xml:space="preserve">
|
||||
<value>Custom colors</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ColorizationMode_None.Content" xml:space="preserve">
|
||||
<value>None</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ColorizationMode_CustomColor.Content" xml:space="preserve">
|
||||
<value>Custom color</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ColorizationMode_WindowsAccent.Content" xml:space="preserve">
|
||||
<value>Accent color</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ColorizationMode_Image.Content" xml:space="preserve">
|
||||
<value>Image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImage_ChooseImageButton.Content" xml:space="preserve">
|
||||
<value>Browse...</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImage_ResetImageButton.Content" xml:space="preserve">
|
||||
<value>Remove image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundColor_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background color</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundColor_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose a custom background color or use the current accent color</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImage_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
||||
<value>No settings</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_Background_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_Background_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose a custom background color or image</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
|
||||
<value>System accent color</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_WindowsAccentColor_OpenWindowsColorsLinkText.Text" xml:space="preserve">
|
||||
<value>Personalization › Colors</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImageBlur_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image blur</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImageBrightness_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image brightness</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Restore defaults</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_Background_ResetImagePropertiesButton.Content" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1.Text" xml:space="preserve">
|
||||
<value>Change the system accent in Windows Settings:</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_Mode_Light_Automation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Light</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_Mode_Dark_Automation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Dark</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AppTheme_Mode_System_Automation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Use system settings</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -4,37 +4,5 @@
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<!-- For slightly adjust the LayerOnAcrylicFillColorDefault color so that the cursor of the searchbox shows -->
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<!-- This is a local copy of LayerOnAcrylicFillColorDefaultBrush -->
|
||||
<SolidColorBrush
|
||||
x:Key="LayerOnAcrylicPrimaryBackgroundBrush"
|
||||
Opacity="0.3"
|
||||
Color="#222222" />
|
||||
<SolidColorBrush
|
||||
x:Key="LayerOnAcrylicSecondaryBackgroundBrush"
|
||||
Opacity="0.0"
|
||||
Color="#222222" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush
|
||||
x:Key="LayerOnAcrylicPrimaryBackgroundBrush"
|
||||
Opacity="0.65"
|
||||
Color="#FFFFFF" />
|
||||
<!-- Because we are tweaking the LayerOnAcrylicPrimaryBackgroundBrush, we need to tweak the command bar background too. If not, it's too bright. -->
|
||||
<SolidColorBrush
|
||||
x:Key="LayerOnAcrylicSecondaryBackgroundBrush"
|
||||
Opacity="0.4"
|
||||
Color="#FFFFFF" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<!-- This is a local copy of LayerOnAcrylicFillColorDefaultBrush -->
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="{ThemeResource LayerOnAcrylicFillColorDefault}" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="Transparent" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<StaticResource x:Key="LayerOnAcrylicPrimaryBackgroundBrush" ResourceKey="LayerOnAccentAcrylicFillColorDefaultBrush" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="Transparent" />
|
||||
<StaticResource x:Key="CmdPal.CommandBarBorderBrush" ResourceKey="CardStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.TopBarBorderBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.DividerStrokeColorDefaultBrush" ResourceKey="CardStrokeColorDefaultBrush" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="#A0FFFFFF" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="Transparent" />
|
||||
<StaticResource x:Key="CmdPal.CommandBarBorderBrush" ResourceKey="CardStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.TopBarBorderBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.DividerStrokeColorDefaultBrush" ResourceKey="CardStrokeColorDefaultBrush" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="{ThemeResource SystemColorWindowColor}" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="{ThemeResource SystemColorWindowColor}" />
|
||||
<SolidColorBrush x:Key="CmdPal.CommandBarBorderBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
|
||||
<SolidColorBrush x:Key="CmdPal.TopBarBorderBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
|
||||
<SolidColorBrush x:Key="CmdPal.DividerStrokeColorDefaultBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="#50202020" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="Transparent" />
|
||||
<StaticResource x:Key="CmdPal.CommandBarBorderBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.TopBarBorderBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.DividerStrokeColorDefaultBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<!--
|
||||
TextBox caret is rendered as inverted and needs clearly-defined background
|
||||
https://github.com/zadjii-msft/PowerToys/issues/348
|
||||
Because we are tweaking the LayerOnAcrylicPrimaryBackgroundBrush, we need to tweak the command bar background too. If not, it's too bright.
|
||||
-->
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="#A0FFFFFF" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="#66FFFFFF" />
|
||||
<StaticResource x:Key="CmdPal.CommandBarBorderBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.TopBarBorderBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
<StaticResource x:Key="CmdPal.DividerStrokeColorDefaultBrush" ResourceKey="DividerStrokeColorDefaultBrush" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="{ThemeResource SystemColorWindowColor}" />
|
||||
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="{ThemeResource SystemColorWindowColor}" />
|
||||
<SolidColorBrush x:Key="CmdPal.CommandBarBorderBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
|
||||
<SolidColorBrush x:Key="CmdPal.TopBarBorderBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
|
||||
<SolidColorBrush x:Key="CmdPal.DividerStrokeColorDefaultBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -5,7 +5,6 @@
|
||||
using System;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
@@ -23,14 +22,34 @@ internal sealed partial class SampleListPageWithDetails : ListPage
|
||||
return [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This page demonstrates Details on ListItems",
|
||||
Title = "Details on ListItems (Small)",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = "List Item 1",
|
||||
Title = "This item has default details size",
|
||||
Body = "Each of these items can have a `Body` formatted with **Markdown**",
|
||||
},
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Details on ListItems (Medium)",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = "This item has medium details size",
|
||||
Body = "Each of these items can have a `Body` formatted with **Markdown**",
|
||||
Size = ContentSize.Medium,
|
||||
},
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Details on ListItems (Large)",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = "This item has large details size",
|
||||
Body = "Each of these items can have a `Body` formatted with **Markdown**",
|
||||
Size = ContentSize.Large,
|
||||
},
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This one has a subtitle too",
|
||||
Subtitle = "Example Subtitle",
|
||||
@@ -70,11 +89,13 @@ internal sealed partial class SampleListPageWithDetails : ListPage
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This one has metadata",
|
||||
Subtitle = "And Large Details panel",
|
||||
Tags = [],
|
||||
Details = new Details()
|
||||
{
|
||||
Title = "Metadata Example",
|
||||
Body = "Each of the sections below is some sample metadata",
|
||||
Size = ContentSize.Large,
|
||||
Metadata = [
|
||||
new DetailsElement()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension.Pages.SectionsPages;
|
||||
|
||||
internal sealed partial class SampleListPageWithSections : ListPage
|
||||
{
|
||||
public SampleListPageWithSections()
|
||||
{
|
||||
Icon = new IconInfo("\uE7C5");
|
||||
Name = "Sample Gallery List Page";
|
||||
}
|
||||
|
||||
public SampleListPageWithSections(IGridProperties gridProperties)
|
||||
{
|
||||
Icon = new IconInfo("\uE7C5");
|
||||
Name = "Sample Gallery List Page";
|
||||
GridProperties = gridProperties;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
var sectionList = new Section("This is a section list", [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Sample Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
]);
|
||||
var anotherSectionList = new Section("This is another section list", [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Another Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "More Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Stop With The Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
]);
|
||||
|
||||
var yesTheresAnother = new Section("There's another", [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Sample Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Another Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "More Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Stop With The Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Another Title",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "More Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Stop With The Titles",
|
||||
Subtitle = "I don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
]);
|
||||
|
||||
return [
|
||||
..sectionList,
|
||||
..anotherSectionList,
|
||||
new Separator(),
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Separators also work",
|
||||
Subtitle = "But I still don't do anything",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
..yesTheresAnother
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using SamplePagesExtension.Pages.SectionsPages;
|
||||
|
||||
namespace SamplePagesExtension.Pages;
|
||||
|
||||
internal sealed partial class SectionsIndexPage : ListPage
|
||||
{
|
||||
public SectionsIndexPage()
|
||||
{
|
||||
Name = "Sections Index Page";
|
||||
Icon = new IconInfo("\uF168");
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
return [
|
||||
new ListItem(new SampleListPageWithSections())
|
||||
{
|
||||
Title = "A list page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new SmallGridLayout()))
|
||||
{
|
||||
Title = "A small grid page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new MediumGridLayout()))
|
||||
{
|
||||
Title = "A medium grid page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new GalleryGridLayout()))
|
||||
{
|
||||
Title = "A Gallery grid page with sections",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "List Page With Details",
|
||||
Subtitle = "A list of items, each with additional details to display",
|
||||
},
|
||||
new ListItem(new SectionsIndexPage())
|
||||
{
|
||||
Title = "List Pages With Sections",
|
||||
Subtitle = "A list of items, with sections header",
|
||||
},
|
||||
new ListItem(new SampleUpdatingItemsPage())
|
||||
{
|
||||
Title = "List page with items that change",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Details : BaseObservable, IDetails
|
||||
public partial class Details : BaseObservable, IDetails, IExtendedAttributesProvider
|
||||
{
|
||||
public virtual IIconInfo HeroImage
|
||||
{
|
||||
@@ -53,4 +54,21 @@ public partial class Details : BaseObservable, IDetails
|
||||
}
|
||||
|
||||
= [];
|
||||
|
||||
public virtual ContentSize Size
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Size));
|
||||
}
|
||||
}
|
||||
|
||||
= ContentSize.Small;
|
||||
|
||||
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
{ "Size", (int)Size },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed partial class Section : IEnumerable<IListItem>
|
||||
{
|
||||
public IListItem[] Items { get; set; } = [];
|
||||
|
||||
public string SectionTitle { get; set; } = string.Empty;
|
||||
|
||||
private Separator CreateSectionListItem()
|
||||
{
|
||||
return new Separator(SectionTitle);
|
||||
}
|
||||
|
||||
public Section(string sectionName, IListItem[] items)
|
||||
{
|
||||
SectionTitle = sectionName;
|
||||
var listItems = items.ToList();
|
||||
|
||||
if (listItems.Count > 0)
|
||||
{
|
||||
listItems.Insert(0, CreateSectionListItem());
|
||||
Items = [.. listItems];
|
||||
}
|
||||
}
|
||||
|
||||
public Section()
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerator<IListItem> GetEnumerator() => Items.ToList().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -4,6 +4,40 @@
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem
|
||||
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
{
|
||||
public Separator(string? title = "")
|
||||
: base()
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
Command = null;
|
||||
}
|
||||
|
||||
public IDetails? Details => null;
|
||||
|
||||
public string? Section { get; private set; }
|
||||
|
||||
public ITag[]? Tags => null;
|
||||
|
||||
public string? TextToSuggest => null;
|
||||
|
||||
public ICommand? Command { get; private set; }
|
||||
|
||||
public IIconInfo? Icon => null;
|
||||
|
||||
public IContextItem[]? MoreCommands => null;
|
||||
|
||||
public string? Subtitle => null;
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => Section;
|
||||
set => Section = value;
|
||||
}
|
||||
|
||||
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,15 @@ namespace Microsoft.CommandPalette.Extensions
|
||||
[uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")]
|
||||
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||
interface IDetailsData {}
|
||||
|
||||
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||
enum ContentSize
|
||||
{
|
||||
Small = 0,
|
||||
Medium = 1,
|
||||
Large = 2,
|
||||
};
|
||||
|
||||
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||
interface IDetailsElement {
|
||||
String Key { get; };
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PathToRoot>..\..\..\..\..\</PathToRoot>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.8.250907003</WasdkNuget>
|
||||
<CppWinRTNuget>$(PathToRoot)packages\Microsoft.Windows.CppWinRT.2.0.240111.5</CppWinRTNuget>
|
||||
<WindowsSdkBuildToolsNuget>$(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188</WindowsSdkBuildToolsNuget>
|
||||
<WindowsSdkBuildToolsNuget>$(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.6901</WindowsSdkBuildToolsNuget>
|
||||
<WebView2Nuget>$(PathToRoot)packages\Microsoft.Web.WebView2.1.0.2903.40</WebView2Nuget>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.6901" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
|
||||
</packages>
|
||||
</packages>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user