mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 19:09:43 +02:00
Compare commits
4 Commits
yuleng/pd/
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ab6e182f8 | ||
|
|
c1ef511697 | ||
|
|
188f6a58a8 | ||
|
|
9449ae3686 |
@@ -48,6 +48,7 @@
|
||||
|
||||
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
|
||||
<Style.Setters>
|
||||
<Setter Property="AutomationProperties.LiveSetting" Value="Off" />
|
||||
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
|
||||
<Setter Property="Padding" Value="{StaticResource DockItemPadding}" />
|
||||
|
||||
@@ -10,7 +10,6 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
@@ -663,6 +662,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
var borderColor = showFrame ? DWMWA_COLOR_DEFAULT : DWMWA_COLOR_NONE;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, &borderColor, sizeof(uint));
|
||||
}
|
||||
|
||||
RedrawWindow(_hwnd);
|
||||
}
|
||||
|
||||
private void InitializeBackdropSupport()
|
||||
@@ -727,7 +728,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
var positionWindowForAnchor = (HWND hwnd) =>
|
||||
{
|
||||
PInvoke.GetWindowRect(hwnd, out var bounds);
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED;
|
||||
switch (anchorCorner)
|
||||
{
|
||||
case AnchorPoint.TopLeft:
|
||||
@@ -827,17 +828,33 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// topmost status when we hide the window (because we cloak it instead
|
||||
// of hiding it).
|
||||
//
|
||||
// SWP_FRAMECHANGED is load-bearing for the borderless look on a cold
|
||||
// start. Asking for SWP_FRAMECHANGED here re-sends WM_NCCALCSIZE and
|
||||
// forces the NC repaint every time we show, so the frame is gone from
|
||||
// the very first summon.
|
||||
// SWP_FRAMECHANGED re-sends WM_NCCALCSIZE so the OS recomputes the
|
||||
// (zero-width) frame. But on this cloak->show path it doesn't reliably
|
||||
// *repaint* the non-client area, so the WS_THICKFRAME border that was
|
||||
// painted earlier survives on the top/left/right edges (the bottom is
|
||||
// clipped away by the card region). A real resize fixes it because it
|
||||
// forces that NC repaint - so we do the same explicitly below.
|
||||
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED);
|
||||
|
||||
// Force the non-client frame to actually redraw (RDW_FRAME) and the
|
||||
// client to repaint over wherever it used to be. Without this the stale
|
||||
// border lingers until the user resizes the window.
|
||||
RedrawWindow(hwnd);
|
||||
|
||||
// Treat the overall show/hide lifecycle as the authoritative
|
||||
// visibility transition, not the lower-level cloak/uncloak helpers.
|
||||
SetIsVisibleToUser(true);
|
||||
}
|
||||
|
||||
private static void RedrawWindow(HWND hwnd)
|
||||
{
|
||||
const uint RDW_INVALIDATE = 0x0001;
|
||||
const uint RDW_UPDATENOW = 0x0100;
|
||||
const uint RDW_ALLCHILDREN = 0x0080;
|
||||
const uint RDW_FRAME = 0x0400;
|
||||
_ = RedrawWindow(hwnd, IntPtr.Zero, IntPtr.Zero, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN | RDW_FRAME);
|
||||
}
|
||||
|
||||
private static DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
||||
{
|
||||
// Leaving a note here, in case we ever need it:
|
||||
@@ -1302,6 +1319,10 @@ public sealed partial class MainWindow : WindowEx,
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, uint flags);
|
||||
|
||||
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
|
||||
@@ -1668,6 +1689,17 @@ public sealed partial class MainWindow : WindowEx,
|
||||
case PInvoke.WM_NCCALCSIZE when wParam.Value != 0 && _hwndFrameVisible != true:
|
||||
return (LRESULT)0;
|
||||
|
||||
// On every activation change the OS repaints the non-client frame to
|
||||
// flip between the active / inactive caption. With WS_THICKFRAME still
|
||||
// present that paints the OS border back over our borderless window each
|
||||
// time we lose (or gain) focus - which is the frame that reappears when
|
||||
// another window steals focus, and the one that shows up shortly after
|
||||
// startup (the first activation flip). Passing -1 as the update-region
|
||||
// (lParam) to DefWindowProc keeps the correct activation result while
|
||||
// telling it to skip the non-client repaint entirely.
|
||||
case PInvoke.WM_NCACTIVATE when _hwndFrameVisible != true:
|
||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, new LPARAM(-1));
|
||||
|
||||
case PInvoke.WM_HOTKEY:
|
||||
{
|
||||
var hotkeyIndex = (int)wParam.Value;
|
||||
|
||||
@@ -105,6 +105,7 @@ WM_ACTIVATE
|
||||
WM_ACTIVATEAPP
|
||||
WA_INACTIVE
|
||||
WM_DISPLAYCHANGE
|
||||
WM_NCACTIVATE
|
||||
WM_SYSCOMMAND
|
||||
WM_SETTINGCHANGE
|
||||
WM_WINDOWPOSCHANGING
|
||||
|
||||
@@ -121,6 +121,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public bool ExpandedMode { get; set; }
|
||||
|
||||
// Item keybindings act on the selected item, which is hidden while collapsed — only honor them when expanded.
|
||||
private bool ItemActionsAllowed => !_compactMode || ExpandedMode;
|
||||
|
||||
public ShellPage()
|
||||
{
|
||||
_settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
|
||||
@@ -949,47 +952,74 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
case VirtualKey.F when modifiers.OnlyAlt: // Alt+F: toggle filter focus
|
||||
((ShellPage)sender).ToggleFilterFocus();
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.Down when modifiers.None:
|
||||
case VirtualKey.Tab when modifiers.None:
|
||||
// In a collapsed compact palette, Down/Tab reveals the top-level items so the
|
||||
// user can browse and discover them. Only swallow the key when we actually
|
||||
// expand; otherwise let it fall through to normal list navigation / focus move.
|
||||
if (((ShellPage)sender).TryExpandCollapsedCompact())
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(modifiers.Ctrl, modifiers.Alt, modifiers.Shift, modifiers.Win, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
// The CommandBar handles item keybindings; skip them while collapsed so a chord can't hit the hidden selection.
|
||||
if (((ShellPage)sender).ItemActionsAllowed)
|
||||
{
|
||||
TryCommandKeybindingMessage msg = new(modifiers.Ctrl, modifiers.Alt, modifiers.Shift, modifiers.Win, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
private void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var mods = KeyModifiers.GetCurrent();
|
||||
if (mods.Ctrl && e.Key == VirtualKey.Enter)
|
||||
if (ItemActionsAllowed && TryHandleItemAction(e))
|
||||
{
|
||||
// ctrl+enter
|
||||
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (mods.Ctrl && e.Key == VirtualKey.K)
|
||||
{
|
||||
// ctrl+k
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Escape)
|
||||
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryHandleItemAction(KeyRoutedEventArgs e)
|
||||
{
|
||||
var mods = KeyModifiers.GetCurrent();
|
||||
switch (e.Key)
|
||||
{
|
||||
// Ctrl+Enter
|
||||
case VirtualKey.Enter when mods.OnlyCtrl:
|
||||
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
|
||||
break;
|
||||
|
||||
// Enter
|
||||
case VirtualKey.Enter when mods.None:
|
||||
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
|
||||
break;
|
||||
|
||||
// Ctrl+K
|
||||
case VirtualKey.K when mods.OnlyCtrl:
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ShellPage_OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1055,6 +1085,23 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
PropertyChanged?.Invoke(this, new(nameof(ExpandedMode)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands a collapsed compact palette on demand (via Down/Tab) so the user can browse the
|
||||
/// top-level items. Returns <see langword="false"/> and does nothing unless compact mode is
|
||||
/// on and the palette is currently collapsed, letting the caller keep the key's normal
|
||||
/// meaning (list navigation / focus traversal) in every other case.
|
||||
/// </summary>
|
||||
private bool TryExpandCollapsedCompact()
|
||||
{
|
||||
if (!_compactMode || ExpandedMode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HandleExpandCompactOnUiThread(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the shell into its compact (collapsed) layout and flushes layout so the host can
|
||||
/// read the resulting card height. Only has an effect when compact mode is enabled.
|
||||
|
||||
@@ -13,8 +13,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class AvailableResultsListTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class FallbackTimeDateItemTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.TimeDate.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Microsoft.CmdPal.Ext.TimeDate;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposable", "CA1001:Types that own disposable fields should be disposable", Justification = "Disposed in TestCleanup")]
|
||||
public class NowDockBandTests
|
||||
{
|
||||
private static readonly DateTime FixedTime = new DateTime(2025, 7, 1, 14, 5, 32);
|
||||
|
||||
private CultureInfo _originalCulture = null!;
|
||||
private CultureInfo _originalUiCulture = null!;
|
||||
private NowDockBand? _band;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_originalCulture = CultureInfo.CurrentCulture;
|
||||
_originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-US", false);
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("en-US", false);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_band?.Dispose();
|
||||
_band = null;
|
||||
CultureInfo.CurrentCulture = _originalCulture;
|
||||
CultureInfo.CurrentUICulture = _originalUiCulture;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_TitleIsSetImmediately()
|
||||
{
|
||||
_band = new NowDockBand(clock: () => FixedTime);
|
||||
|
||||
Assert.AreEqual("2:05 PM", _band.Title);
|
||||
Assert.IsFalse(string.IsNullOrEmpty(_band.Subtitle));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateText_LongTimeFormat_TitleContainsSeconds()
|
||||
{
|
||||
_band = new NowDockBand(timeWithSeconds: true, clock: () => FixedTime);
|
||||
|
||||
_band.UpdateText();
|
||||
|
||||
Assert.AreEqual("2:05:32 PM", _band.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateText_ShortDateFormat_SubtitleIsShortDate()
|
||||
{
|
||||
_band = new NowDockBand(clock: () => FixedTime);
|
||||
|
||||
_band.UpdateText();
|
||||
|
||||
Assert.AreEqual("7/1/2025", _band.Subtitle);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateText_FiresOnUpdatedCallback()
|
||||
{
|
||||
var callbackFired = false;
|
||||
_band = new NowDockBand(onUpdated: () => callbackFired = true, clock: () => FixedTime);
|
||||
|
||||
callbackFired = false; // reset — constructor already fired it once during synchronous UpdateText()
|
||||
|
||||
_band.UpdateText();
|
||||
|
||||
Assert.IsTrue(callbackFired);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateText_CallbackFiredAfterAssignments()
|
||||
{
|
||||
var titleAtCallback = string.Empty;
|
||||
_band = new NowDockBand(
|
||||
onUpdated: () => titleAtCallback = _band?.Title ?? string.Empty,
|
||||
clock: () => FixedTime);
|
||||
|
||||
titleAtCallback = string.Empty; // reset after construction callback
|
||||
|
||||
_band.UpdateText();
|
||||
|
||||
Assert.IsFalse(string.IsNullOrEmpty(titleAtCallback), "Title should be assigned before callback fires");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateText_CopyCommandsUpdated()
|
||||
{
|
||||
_band = new NowDockBand(clock: () => FixedTime);
|
||||
|
||||
_band.UpdateText();
|
||||
|
||||
Assert.AreEqual(_band.Title, _band.CopyTimeCommand.Text);
|
||||
Assert.AreEqual(_band.Subtitle, _band.CopyDateCommand.Text);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("de-DE")]
|
||||
[DataRow("fr-FR")]
|
||||
[DataRow("ar-SA")]
|
||||
public void UpdateText_CultureSmoke_TitleNonEmpty(string cultureName)
|
||||
{
|
||||
// Culture MUST be set before construction — constructor calls UpdateText() synchronously
|
||||
CultureInfo.CurrentCulture = new CultureInfo(cultureName, false);
|
||||
CultureInfo.CurrentUICulture = new CultureInfo(cultureName, false);
|
||||
|
||||
_band = new NowDockBand(clock: () => FixedTime);
|
||||
|
||||
Assert.IsFalse(string.IsNullOrEmpty(_band.Title), $"Title should be non-empty for culture '{cultureName}'");
|
||||
Assert.IsFalse(string.IsNullOrEmpty(_band.Subtitle), $"Subtitle should be non-empty for culture '{cultureName}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateSettings_EnablingSeconds_TitleIncludesSeconds()
|
||||
{
|
||||
_band = new NowDockBand(timeWithSeconds: false, clock: () => FixedTime);
|
||||
|
||||
Assert.AreEqual("2:05 PM", _band.Title, "Precondition: seconds hidden by default");
|
||||
|
||||
_band.UpdateSettings(timeWithSeconds: true);
|
||||
|
||||
Assert.AreEqual("2:05:32 PM", _band.Title, "Title should update live to include seconds");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateSettings_DisablingSeconds_TitleDropsSeconds()
|
||||
{
|
||||
_band = new NowDockBand(timeWithSeconds: true, clock: () => FixedTime);
|
||||
|
||||
Assert.AreEqual("2:05:32 PM", _band.Title, "Precondition: seconds shown");
|
||||
|
||||
_band.UpdateSettings(timeWithSeconds: false);
|
||||
|
||||
Assert.AreEqual("2:05 PM", _band.Title, "Title should update live to drop seconds");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateSettings_NoChange_FiresNoCallback()
|
||||
{
|
||||
var callbackCount = 0;
|
||||
_band = new NowDockBand(timeWithSeconds: false, onUpdated: () => callbackCount++, clock: () => FixedTime);
|
||||
|
||||
callbackCount = 0; // reset after construction callback
|
||||
|
||||
_band.UpdateSettings(timeWithSeconds: false);
|
||||
|
||||
Assert.AreEqual(0, callbackCount, "A no-op settings change should not refresh the band");
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
@@ -166,7 +166,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
Assert.IsTrue(results.Length > 0, $"Query '{query}' should return at least one result");
|
||||
|
||||
var firstItem = results.FirstOrDefault();
|
||||
Assert.IsTrue(firstItem.Title.StartsWith("Error: Invalid input", StringComparison.CurrentCulture), $"Query '{query}' should return an error result for invalid input");
|
||||
Assert.IsTrue(firstItem!.Title.StartsWith("Error: Invalid input", StringComparison.CurrentCulture), $"Query '{query}' should return an error result for invalid input");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -201,7 +201,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
var firstResult = results.FirstOrDefault();
|
||||
Assert.IsTrue(firstResult.Subtitle.StartsWith(expectedSubtitle, StringComparison.CurrentCulture), $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
|
||||
Assert.IsTrue(firstResult!.Subtitle.StartsWith(expectedSubtitle, StringComparison.CurrentCulture), $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -218,7 +218,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
// Assert
|
||||
Assert.IsNotNull(resultsList);
|
||||
var firstResult = resultsList.FirstOrDefault();
|
||||
Assert.IsTrue(firstResult.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
|
||||
Assert.IsTrue(firstResult!.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class ResultHelperTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
@@ -41,6 +41,8 @@ public class ResultHelperTests
|
||||
{
|
||||
Label = "Test Label",
|
||||
Value = "Test Value",
|
||||
AlternativeSearchTag = string.Empty,
|
||||
IconType = ResultIconType.Time,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -55,10 +57,10 @@ public class ResultHelperTests
|
||||
[TestMethod]
|
||||
public void ResultHelper_CreateListItem_HandlesNullInput()
|
||||
{
|
||||
AvailableResult availableResult = null;
|
||||
AvailableResult? availableResult = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsException<System.NullReferenceException>(() => availableResult.ToListItem());
|
||||
Assert.ThrowsException<System.NullReferenceException>(() => availableResult!.ToListItem());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -69,6 +71,8 @@ public class ResultHelperTests
|
||||
{
|
||||
Label = string.Empty,
|
||||
Value = string.Empty,
|
||||
AlternativeSearchTag = string.Empty,
|
||||
IconType = ResultIconType.Time,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -88,6 +92,7 @@ public class ResultHelperTests
|
||||
{
|
||||
Label = "Test Label",
|
||||
Value = "Test Value",
|
||||
AlternativeSearchTag = string.Empty,
|
||||
IconType = ResultIconType.Date,
|
||||
};
|
||||
|
||||
@@ -110,6 +115,8 @@ public class ResultHelperTests
|
||||
{
|
||||
Label = longText,
|
||||
Value = longText,
|
||||
AlternativeSearchTag = string.Empty,
|
||||
IconType = ResultIconType.Time,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -130,6 +137,8 @@ public class ResultHelperTests
|
||||
{
|
||||
Label = specialText,
|
||||
Value = specialText,
|
||||
AlternativeSearchTag = string.Empty,
|
||||
IconType = ResultIconType.Time,
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -13,6 +13,7 @@ public class Settings : ISettingsInterface
|
||||
private readonly int firstDayOfWeek;
|
||||
private readonly bool enableFallbackItems;
|
||||
private readonly bool timeWithSecond;
|
||||
private readonly bool dockClockWithSecond;
|
||||
private readonly bool dateWithWeekday;
|
||||
private readonly List<string> customFormats;
|
||||
|
||||
@@ -21,13 +22,15 @@ public class Settings : ISettingsInterface
|
||||
int firstDayOfWeek = -1,
|
||||
bool enableFallbackItems = true,
|
||||
bool timeWithSecond = false,
|
||||
bool dockClockWithSecond = false,
|
||||
bool dateWithWeekday = false,
|
||||
List<string> customFormats = null)
|
||||
List<string>? customFormats = null)
|
||||
{
|
||||
this.firstWeekOfYear = firstWeekOfYear;
|
||||
this.firstDayOfWeek = firstDayOfWeek;
|
||||
this.enableFallbackItems = enableFallbackItems;
|
||||
this.timeWithSecond = timeWithSecond;
|
||||
this.dockClockWithSecond = dockClockWithSecond;
|
||||
this.dateWithWeekday = dateWithWeekday;
|
||||
this.customFormats = customFormats ?? new List<string>();
|
||||
}
|
||||
@@ -40,6 +43,8 @@ public class Settings : ISettingsInterface
|
||||
|
||||
public bool TimeWithSecond => timeWithSecond;
|
||||
|
||||
public bool DockClockWithSecond => dockClockWithSecond;
|
||||
|
||||
public bool DateWithWeekday => dateWithWeekday;
|
||||
|
||||
public List<string> CustomFormats => customFormats;
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class StringParserTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class TimeAndDateHelperTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
|
||||
[TestClass]
|
||||
public class TimeDateCalculatorTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
@@ -69,7 +69,7 @@ public class TimeDateCalculatorTests
|
||||
var settings = new SettingsManager();
|
||||
|
||||
// Act
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, null);
|
||||
var results = TimeDateCalculator.ExecuteSearch(settings, null!);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(results);
|
||||
|
||||
@@ -11,8 +11,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
|
||||
[TestClass]
|
||||
public class TimeDateCommandsProviderTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
private CultureInfo originalCulture = null!;
|
||||
private CultureInfo originalUiCulture = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
@@ -90,5 +90,27 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
|
||||
// Assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(displayName));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetDockBands_ReturnsNonEmptyArray()
|
||||
{
|
||||
var provider = new TimeDateCommandsProvider();
|
||||
|
||||
var bands = provider.GetDockBands();
|
||||
|
||||
Assert.IsTrue(bands.Length > 0, "GetDockBands should return at least one item");
|
||||
Assert.IsNotNull(bands[0], "First dock band should not be null");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetDockBands_NotificationCenterBandDoesNotSetDockIcon()
|
||||
{
|
||||
var provider = new TimeDateCommandsProvider();
|
||||
|
||||
var bands = provider.GetDockBands();
|
||||
|
||||
Assert.IsTrue(bands.Length > 1, "Expected notification center band to be present");
|
||||
Assert.IsNull(bands[1].Icon, "Notification center band should not set a dock icon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
|
||||
|
||||
_validOptions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", CultureInfo.CurrentCulture)!,
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", CultureInfo.CurrentCulture)!,
|
||||
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", CultureInfo.CurrentCulture)!,
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", CultureInfo.CurrentCulture)!,
|
||||
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", CultureInfo.CurrentCulture)!,
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", CultureInfo.CurrentCulture)!,
|
||||
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", CultureInfo.CurrentCulture),
|
||||
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", CultureInfo.CurrentCulture)!,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
|
||||
}
|
||||
|
||||
var availableResults = AvailableResultsList.GetList(false, _settingsManager, timestamp: _timestamp);
|
||||
ListItem result = null;
|
||||
ListItem? result = null;
|
||||
var maxScore = 0;
|
||||
|
||||
foreach (var f in availableResults)
|
||||
|
||||
@@ -10,22 +10,22 @@ internal sealed class AvailableResult
|
||||
/// <summary>
|
||||
/// Gets or sets the time/date value
|
||||
/// </summary>
|
||||
internal string Value { get; set; }
|
||||
internal required string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text used for the subtitle and as search term
|
||||
/// </summary>
|
||||
internal string Label { get; set; }
|
||||
internal required string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an alternative search tag that will be evaluated if label doesn't match. For example we like to show the era on searches for 'year' too.
|
||||
/// </summary>
|
||||
internal string AlternativeSearchTag { get; set; }
|
||||
internal required string AlternativeSearchTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the type of result
|
||||
/// </summary>
|
||||
internal ResultIconType IconType { get; set; }
|
||||
internal required ResultIconType IconType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value to show additional error details
|
||||
@@ -37,7 +37,7 @@ internal sealed class AvailableResult
|
||||
/// </summary>
|
||||
/// <param name="theme">Theme</param>
|
||||
/// <returns>Path</returns>
|
||||
public IconInfo GetIconInfo()
|
||||
public IconInfo? GetIconInfo()
|
||||
{
|
||||
return IconType switch
|
||||
{
|
||||
|
||||
@@ -20,6 +20,8 @@ public interface ISettingsInterface
|
||||
|
||||
public bool TimeWithSecond { get; }
|
||||
|
||||
public bool DockClockWithSecond { get; }
|
||||
|
||||
public bool DateWithWeekday { get; }
|
||||
|
||||
public List<string> CustomFormats { get; }
|
||||
|
||||
@@ -17,7 +17,7 @@ internal static class ResultHelper
|
||||
/// <param name="stringId">Id of the string. (Example: `MyString` for `MyString` and `MyStringNow`)</param>
|
||||
/// <param name="stringIdNow">Optional string id for now case</param>
|
||||
/// <returns>The string from the resource file, or <see cref="string.Empty"/> otherwise.</returns>
|
||||
internal static string SelectStringFromResources(bool isSystemTimeDate, string stringId, string stringIdNow = default)
|
||||
internal static string SelectStringFromResources(bool isSystemTimeDate, string stringId, string? stringIdNow = default)
|
||||
{
|
||||
return !isSystemTimeDate
|
||||
? Resources.ResourceManager.GetString(stringId, CultureInfo.CurrentUICulture) ?? string.Empty
|
||||
|
||||
@@ -87,6 +87,12 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Resources.Microsoft_plugin_timedate_SettingTimeWithSeconds_Description,
|
||||
false); // TODO -- double check default value
|
||||
|
||||
private readonly ToggleSetting _dockClockWithSeconds = new(
|
||||
Namespaced(nameof(DockClockWithSecond)),
|
||||
Resources.Microsoft_plugin_timedate_SettingDockClockWithSeconds,
|
||||
Resources.Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description,
|
||||
false);
|
||||
|
||||
private readonly ToggleSetting _dateWithWeekday = new(
|
||||
Namespaced(nameof(DateWithWeekday)),
|
||||
Resources.Microsoft_plugin_timedate_SettingDateWithWeekday,
|
||||
@@ -143,9 +149,11 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
public bool TimeWithSecond => _timeWithSeconds.Value;
|
||||
|
||||
public bool DockClockWithSecond => _dockClockWithSeconds.Value;
|
||||
|
||||
public bool DateWithWeekday => _dateWithWeekday.Value;
|
||||
|
||||
public List<string> CustomFormats => _customFormats.Value.Split(TEXTBOXNEWLINE).ToList();
|
||||
public List<string> CustomFormats => (_customFormats.Value ?? string.Empty).Split(TEXTBOXNEWLINE).ToList();
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
@@ -161,6 +169,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
Settings.Add(_enableFallbackItems);
|
||||
Settings.Add(_timeWithSeconds);
|
||||
Settings.Add(_dockClockWithSeconds);
|
||||
Settings.Add(_dateWithWeekday);
|
||||
Settings.Add(_firstWeekOfYear);
|
||||
Settings.Add(_firstDayOfWeek);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.TimeDate</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.TimeDate;
|
||||
|
||||
internal sealed partial class NowDockBand : ListItem, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan PerSecondUpdateInterval = TimeSpan.FromSeconds(1);
|
||||
private static readonly TimeSpan PerMinuteUpdateInterval = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly System.Timers.Timer _timer;
|
||||
private readonly Action? _onUpdated;
|
||||
private readonly Func<DateTime> _clock;
|
||||
private bool _timeWithSeconds;
|
||||
|
||||
private CopyTextCommand _copyTimeCommand;
|
||||
private CopyTextCommand _copyDateCommand;
|
||||
|
||||
internal CopyTextCommand CopyTimeCommand => _copyTimeCommand;
|
||||
|
||||
internal CopyTextCommand CopyDateCommand => _copyDateCommand;
|
||||
|
||||
internal NowDockBand(bool timeWithSeconds = false, Action? onUpdated = null, Func<DateTime>? clock = null)
|
||||
{
|
||||
_timeWithSeconds = timeWithSeconds;
|
||||
_onUpdated = onUpdated;
|
||||
_clock = clock ?? (() => DateTime.Now);
|
||||
|
||||
Command = new OpenUrlCommand("ms-actioncenter:")
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.timedate.dockBand",
|
||||
Name = Resources.timedate_show_notification_center_command_name,
|
||||
Result = CommandResult.Dismiss(),
|
||||
};
|
||||
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
|
||||
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
|
||||
MoreCommands =
|
||||
[
|
||||
new CommandContextItem(_copyTimeCommand),
|
||||
new CommandContextItem(_copyDateCommand),
|
||||
];
|
||||
|
||||
UpdateText();
|
||||
|
||||
_timer = new System.Timers.Timer() { AutoReset = true };
|
||||
ConfigureTimer();
|
||||
}
|
||||
|
||||
// Reads the current "show seconds" preference and, if it changed, reconfigures
|
||||
// the timer cadence and refreshes the displayed text. Safe to call at any time
|
||||
// (e.g. from a settings-changed handler) so the dock clock stays in sync without
|
||||
// requiring the app to restart.
|
||||
internal void UpdateSettings(bool timeWithSeconds)
|
||||
{
|
||||
if (_timeWithSeconds == timeWithSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timeWithSeconds = timeWithSeconds;
|
||||
ConfigureTimer();
|
||||
UpdateText();
|
||||
}
|
||||
|
||||
private void ConfigureTimer()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Elapsed -= Timer_Elapsed;
|
||||
_timer.Elapsed -= Timer_ElapsedFirstMinuteTick;
|
||||
|
||||
if (_timeWithSeconds)
|
||||
{
|
||||
_timer.Interval = PerSecondUpdateInterval.TotalMilliseconds;
|
||||
_timer.Elapsed += Timer_Elapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Align the first tick to the next minute boundary so the clock flips
|
||||
// exactly when the system clock does, then fall back to a per-minute cadence.
|
||||
var now = _clock();
|
||||
_timer.Interval = PerMinuteUpdateInterval.TotalMilliseconds - ((now.Second * 1000) + now.Millisecond);
|
||||
_timer.Elapsed += Timer_ElapsedFirstMinuteTick;
|
||||
}
|
||||
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void Timer_ElapsedFirstMinuteTick(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if (sender is System.Timers.Timer timer)
|
||||
{
|
||||
timer.Interval = PerMinuteUpdateInterval.TotalMilliseconds;
|
||||
timer.Elapsed -= Timer_ElapsedFirstMinuteTick;
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
}
|
||||
|
||||
Timer_Elapsed(sender, e);
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
UpdateText();
|
||||
}
|
||||
|
||||
internal void UpdateText()
|
||||
{
|
||||
var now = _clock();
|
||||
var timeString = now.ToString(
|
||||
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, _timeWithSeconds, false),
|
||||
CultureInfo.CurrentCulture);
|
||||
var dateString = now.ToString(
|
||||
TimeAndDateHelper.GetStringFormat(FormatStringType.Date, false, false),
|
||||
CultureInfo.CurrentCulture);
|
||||
|
||||
Title = timeString;
|
||||
Subtitle = dateString;
|
||||
_copyTimeCommand.Text = timeString;
|
||||
_copyDateCommand.Text = dateString;
|
||||
|
||||
_onUpdated?.Invoke(); // Must remain last — ViewModel reads Title/Subtitle via GetItems() on callback
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -834,6 +834,24 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show seconds on the dock clock.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_timedate_SettingDockClockWithSeconds {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_timedate_SettingDockClockWithSeconds", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This setting applies to the clock shown on the Command Palette dock..
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select for more details..
|
||||
/// </summary>
|
||||
|
||||
@@ -267,6 +267,12 @@
|
||||
<data name="Microsoft_plugin_timedate_SettingTimeWithSeconds_Description" xml:space="preserve">
|
||||
<value>This setting applies to the 'Time' and 'Now' result.</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_timedate_SettingDockClockWithSeconds" xml:space="preserve">
|
||||
<value>Show seconds on the dock clock</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description" xml:space="preserve">
|
||||
<value>This setting applies to the clock shown on the Command Palette dock.</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_timedate_SubTitleNote" xml:space="preserve">
|
||||
<value>Select or press Ctrl+C to copy</value>
|
||||
<comment>'Ctrl+C' is a shortcut</comment>
|
||||
|
||||
@@ -20,8 +20,11 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
|
||||
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
|
||||
private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
|
||||
|
||||
private readonly ListItem _bandItem;
|
||||
private readonly ListItem _notificationCenterBandItem;
|
||||
private readonly WrappedDockItem _bandItem;
|
||||
private readonly WrappedDockItem _notificationCenterBandItem;
|
||||
|
||||
// Keep a reference to the band so we can dispose it when the provider is disposed.
|
||||
private NowDockBand? _nowDockBand;
|
||||
|
||||
public TimeDateCommandsProvider()
|
||||
{
|
||||
@@ -37,8 +40,40 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
|
||||
Icon = _timeDateExtensionPage.Icon;
|
||||
Settings = _settingsManager.Settings;
|
||||
|
||||
_bandItem = new NowDockBand();
|
||||
_notificationCenterBandItem = new NotificationCenterDockBand();
|
||||
WrappedDockItem? wrappedBand = null;
|
||||
|
||||
// During NowDockBand construction, UpdateText() runs synchronously.
|
||||
// At that point wrappedBand is still null so the callback is a no-op.
|
||||
// On subsequent timer ticks, wrappedBand is non-null and SetItems fires
|
||||
// RaiseItemsChanged - the framework marshals to the UI thread in
|
||||
// DockBandViewModel.InitializeFromList via DoOnUiThread.
|
||||
_nowDockBand = new NowDockBand(_settingsManager.DockClockWithSecond, onUpdated: () =>
|
||||
{
|
||||
if (wrappedBand is not null)
|
||||
{
|
||||
wrappedBand.Items = [_nowDockBand!];
|
||||
}
|
||||
});
|
||||
|
||||
// Re-read the dock clock preference whenever settings change so the band updates
|
||||
// live (no app restart required). The band ignores no-op changes internally.
|
||||
_settingsManager.Settings.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
wrappedBand = new WrappedDockItem(
|
||||
[_nowDockBand],
|
||||
"com.microsoft.cmdpal.timedate.dockBand",
|
||||
Resources.Microsoft_plugin_timedate_dock_band_title)
|
||||
{
|
||||
Icon = Icons.TimeDateExtIcon,
|
||||
};
|
||||
|
||||
_bandItem = wrappedBand;
|
||||
|
||||
var notificationCenterBand = new NotificationCenterDockBand();
|
||||
_notificationCenterBandItem = new WrappedDockItem(
|
||||
[notificationCenterBand],
|
||||
"com.microsoft.cmdpal.timedate.notificationCenterBand",
|
||||
Resources.timedate_notification_center_band_title);
|
||||
}
|
||||
|
||||
private string GetTranslatedPluginDescription()
|
||||
@@ -56,97 +91,26 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
|
||||
|
||||
public override ICommandItem[] GetDockBands()
|
||||
{
|
||||
var clockBand = new WrappedDockItem(
|
||||
[_bandItem],
|
||||
"com.microsoft.cmdpal.timedate.dockBand",
|
||||
Resources.Microsoft_plugin_timedate_dock_band_title)
|
||||
{
|
||||
Icon = Icons.TimeDateExtIcon,
|
||||
};
|
||||
return [_bandItem, _notificationCenterBandItem];
|
||||
}
|
||||
|
||||
var notificationBand = new WrappedDockItem(
|
||||
[_notificationCenterBandItem],
|
||||
"com.microsoft.cmdpal.timedate.notificationCenterBand",
|
||||
Resources.timedate_notification_center_band_title)
|
||||
{
|
||||
Icon = Icons.NotificationCenterIcon,
|
||||
};
|
||||
private void OnSettingsChanged(object sender, Settings args)
|
||||
{
|
||||
_nowDockBand?.UpdateSettings(_settingsManager.DockClockWithSecond);
|
||||
}
|
||||
|
||||
return new ICommandItem[] { clockBand, notificationBand };
|
||||
public override void Dispose()
|
||||
{
|
||||
_settingsManager.Settings.SettingsChanged -= OnSettingsChanged;
|
||||
_nowDockBand?.Dispose();
|
||||
_nowDockBand = null;
|
||||
GC.SuppressFinalize(this);
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
internal sealed partial class NowDockBand : ListItem
|
||||
{
|
||||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(60);
|
||||
private CopyTextCommand _copyTimeCommand;
|
||||
private CopyTextCommand _copyDateCommand;
|
||||
|
||||
public NowDockBand()
|
||||
{
|
||||
// Open Notification Center on click
|
||||
Command = new OpenUrlCommand("ms-actioncenter:") { Id = "com.microsoft.cmdpal.timedate.dockBand", Name = Resources.timedate_show_notification_center_command_name, Result = CommandResult.Dismiss(), Icon = null };
|
||||
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
|
||||
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
|
||||
MoreCommands = [
|
||||
new CommandContextItem(_copyTimeCommand),
|
||||
new CommandContextItem(_copyDateCommand)
|
||||
];
|
||||
UpdateText();
|
||||
|
||||
// Create a timer to update the time every minute
|
||||
System.Timers.Timer timer = new(UpdateInterval.TotalMilliseconds);
|
||||
|
||||
// but we want it to tick on the minute, so calculate the initial delay
|
||||
var now = DateTime.Now;
|
||||
timer.Interval = UpdateInterval.TotalMilliseconds - ((now.Second * 1000) + now.Millisecond);
|
||||
|
||||
// then after the first tick, set it to 60 seconds
|
||||
timer.Elapsed += Timer_ElapsedFirst;
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private void Timer_ElapsedFirst(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
// After the first tick, set the interval to 60 seconds
|
||||
if (sender is System.Timers.Timer timer)
|
||||
{
|
||||
timer.Interval = UpdateInterval.TotalMilliseconds;
|
||||
timer.Elapsed -= Timer_ElapsedFirst;
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
|
||||
// Still call the callback, so that we update the clock
|
||||
Timer_Elapsed(sender, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
UpdateText();
|
||||
}
|
||||
|
||||
private void UpdateText()
|
||||
{
|
||||
var timeExtended = false; // timeLongFormat ?? settings.TimeWithSecond;
|
||||
var dateExtended = false; // dateLongFormat ?? settings.DateWithWeekday;
|
||||
var dateTimeNow = DateTime.Now;
|
||||
|
||||
var timeString = dateTimeNow.ToString(
|
||||
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, timeExtended, dateExtended),
|
||||
CultureInfo.CurrentCulture);
|
||||
var dateString = dateTimeNow.ToString(
|
||||
TimeAndDateHelper.GetStringFormat(FormatStringType.Date, timeExtended, dateExtended),
|
||||
CultureInfo.CurrentCulture);
|
||||
|
||||
Title = timeString;
|
||||
Subtitle = dateString;
|
||||
|
||||
_copyDateCommand.Text = dateString;
|
||||
_copyTimeCommand.Text = timeString;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class NotificationCenterDockBand : ListItem
|
||||
{
|
||||
public NotificationCenterDockBand()
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class LightSwitchProfileIdTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void LightSwitchProperties_ProfileIds_RoundTripThroughJson_DefaultZero()
|
||||
{
|
||||
var props = new LightSwitchProperties();
|
||||
Assert.AreEqual(0, props.LightModeProfileId.Value);
|
||||
Assert.AreEqual(0, props.DarkModeProfileId.Value);
|
||||
|
||||
props.LightModeProfileId.Value = 3;
|
||||
props.DarkModeProfileId.Value = 5;
|
||||
|
||||
var json = JsonSerializer.Serialize(props);
|
||||
var back = JsonSerializer.Deserialize<LightSwitchProperties>(json);
|
||||
|
||||
Assert.IsNotNull(back);
|
||||
Assert.AreEqual(3, back!.LightModeProfileId.Value);
|
||||
Assert.AreEqual(5, back.DarkModeProfileId.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LightSwitchSettings_Clone_PreservesProfileIds()
|
||||
{
|
||||
var settings = new LightSwitchSettings();
|
||||
settings.Properties.DarkModeProfileId.Value = 7;
|
||||
settings.Properties.LightModeProfileId.Value = 3;
|
||||
settings.Properties.DarkModeProfile.Value = "Night";
|
||||
settings.Properties.LightModeProfile.Value = "Day";
|
||||
|
||||
var clone = (LightSwitchSettings)settings.Clone();
|
||||
|
||||
Assert.AreEqual(7, clone.Properties.DarkModeProfileId.Value);
|
||||
Assert.AreEqual(3, clone.Properties.LightModeProfileId.Value);
|
||||
Assert.AreEqual("Night", clone.Properties.DarkModeProfile.Value);
|
||||
Assert.AreEqual("Day", clone.Properties.LightModeProfile.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class PowerDisplayProfilesTests
|
||||
{
|
||||
private static PowerDisplayProfile MakeProfile(string name, int id = 0)
|
||||
{
|
||||
var p = new PowerDisplayProfile(name, new List<ProfileMonitorSetting>
|
||||
{
|
||||
new ProfileMonitorSetting("MON1", 50, null, null, null),
|
||||
});
|
||||
p.Id = id;
|
||||
return p;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdAndNextId_RoundTripThroughJson_AndDefaultToZero()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles();
|
||||
var p = MakeProfile("Gaming", id: 7);
|
||||
profiles.Profiles.Add(p);
|
||||
profiles.NextId = 8;
|
||||
|
||||
var json = JsonSerializer.Serialize(profiles, ProfileSerializationContext.Default.PowerDisplayProfiles);
|
||||
var back = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.PowerDisplayProfiles);
|
||||
|
||||
Assert.IsNotNull(back);
|
||||
Assert.AreEqual(8, back!.NextId);
|
||||
Assert.AreEqual(7, back.Profiles[0].Id);
|
||||
Assert.AreEqual(0, new PowerDisplayProfile().Id);
|
||||
Assert.AreEqual(0, new PowerDisplayProfiles().NextId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetById_ReturnsMatch_OrNullForZeroAndMissing()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles();
|
||||
var a = MakeProfile("A", id: 1);
|
||||
var b = MakeProfile("B", id: 2);
|
||||
profiles.Profiles.Add(a);
|
||||
profiles.Profiles.Add(b);
|
||||
|
||||
Assert.AreSame(b, profiles.GetById(2));
|
||||
Assert.IsNull(profiles.GetById(0));
|
||||
Assert.IsNull(profiles.GetById(99));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RemoveProfileById_RemovesWhenPresent()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles();
|
||||
profiles.Profiles.Add(MakeProfile("A", id: 1));
|
||||
profiles.Profiles.Add(MakeProfile("B", id: 2));
|
||||
|
||||
Assert.IsTrue(profiles.RemoveProfile(2));
|
||||
Assert.AreEqual(1, profiles.Profiles.Count);
|
||||
Assert.IsFalse(profiles.RemoveProfile(2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetProfile_AssignsIncreasingIds_AndAllowsDuplicateNames()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles { NextId = 1 };
|
||||
var a = MakeProfile("Same");
|
||||
var b = MakeProfile("Same");
|
||||
|
||||
profiles.SetProfile(a);
|
||||
profiles.SetProfile(b);
|
||||
|
||||
Assert.AreEqual(1, a.Id);
|
||||
Assert.AreEqual(2, b.Id);
|
||||
Assert.AreEqual(3, profiles.NextId);
|
||||
Assert.AreEqual(2, profiles.Profiles.Count); // both kept: duplicate names allowed
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetProfile_WithExplicitId_ReplacesSameIdAndHealsNextId()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles { NextId = 1 };
|
||||
var original = MakeProfile("A", id: 5);
|
||||
profiles.SetProfile(original); // explicit id 5
|
||||
|
||||
var replacement = MakeProfile("A-edited", id: 5);
|
||||
profiles.SetProfile(replacement);
|
||||
|
||||
Assert.AreEqual(1, profiles.Profiles.Count);
|
||||
Assert.AreSame(replacement, profiles.GetById(5));
|
||||
Assert.IsTrue(profiles.NextId > 5); // healed past the explicit id
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EnsureIds_BackfillsInOrder_SetsNextId_AndIsIdempotent()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles(); // NextId defaults to 0 (legacy file)
|
||||
profiles.Profiles.Add(MakeProfile("A")); // Id 0
|
||||
profiles.Profiles.Add(MakeProfile("B")); // Id 0
|
||||
|
||||
Assert.IsTrue(profiles.EnsureIds());
|
||||
Assert.AreEqual(1, profiles.Profiles[0].Id);
|
||||
Assert.AreEqual(2, profiles.Profiles[1].Id);
|
||||
Assert.AreEqual(3, profiles.NextId);
|
||||
|
||||
Assert.IsFalse(profiles.EnsureIds()); // second run: no change
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EnsureIds_SelfHealsNextId_AndPreservesExistingIds()
|
||||
{
|
||||
var profiles = new PowerDisplayProfiles { NextId = 2 }; // corrupt: <= existing max
|
||||
profiles.Profiles.Add(MakeProfile("A", id: 5));
|
||||
profiles.Profiles.Add(MakeProfile("B")); // Id 0
|
||||
|
||||
Assert.IsTrue(profiles.EnsureIds());
|
||||
Assert.AreEqual(5, profiles.Profiles[0].Id); // preserved
|
||||
Assert.AreEqual(6, profiles.Profiles[1].Id); // assigned above max
|
||||
Assert.AreEqual(7, profiles.NextId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DisplayName_CombinesNameAndId()
|
||||
{
|
||||
var p = MakeProfile("Gaming", id: 4);
|
||||
Assert.AreEqual("Gaming (#4)", p.DisplayName);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@ namespace PowerDisplay.Models
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("monitorSettings")]
|
||||
public List<ProfileMonitorSetting> MonitorSettings { get; set; }
|
||||
|
||||
@@ -59,12 +56,5 @@ namespace PowerDisplay.Models
|
||||
{
|
||||
LastModified = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable label that disambiguates duplicate names, e.g. "Gaming (#4)".
|
||||
/// Not serialized; UI display only.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string DisplayName => $"{Name} (#{Id})";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ namespace PowerDisplay.Models
|
||||
[JsonPropertyName("profiles")]
|
||||
public List<PowerDisplayProfile> Profiles { get; set; }
|
||||
|
||||
[JsonPropertyName("nextId")]
|
||||
public int NextId { get; set; }
|
||||
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTime LastUpdated { get; set; }
|
||||
|
||||
@@ -40,17 +37,7 @@ namespace PowerDisplay.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile by its stable id, or null when id is not positive or no profile has it.
|
||||
/// </summary>
|
||||
public PowerDisplayProfile? GetById(int id)
|
||||
{
|
||||
return id <= 0 ? null : Profiles.FirstOrDefault(p => p.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a profile, keyed by its stable id. When the incoming profile has no id
|
||||
/// (Id == 0) a new one is assigned from the monotonic NextId counter. Names are not required
|
||||
/// to be unique.
|
||||
/// Adds or updates a profile
|
||||
/// </summary>
|
||||
public void SetProfile(PowerDisplayProfile profile)
|
||||
{
|
||||
@@ -59,27 +46,10 @@ namespace PowerDisplay.Models
|
||||
throw new ArgumentException("Profile is invalid");
|
||||
}
|
||||
|
||||
if (profile.Id == 0)
|
||||
var existing = GetProfile(profile.Name);
|
||||
if (existing != null)
|
||||
{
|
||||
if (NextId < 1)
|
||||
{
|
||||
NextId = 1;
|
||||
}
|
||||
|
||||
profile.Id = NextId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = GetById(profile.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
Profiles.Remove(existing);
|
||||
}
|
||||
|
||||
if (NextId <= profile.Id)
|
||||
{
|
||||
NextId = profile.Id + 1;
|
||||
}
|
||||
Profiles.Remove(existing);
|
||||
}
|
||||
|
||||
profile.Touch();
|
||||
@@ -104,66 +74,23 @@ namespace PowerDisplay.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a profile by its stable id.
|
||||
/// Checks if a profile name is valid and available
|
||||
/// </summary>
|
||||
public bool RemoveProfile(int id)
|
||||
public bool IsNameAvailable(string name, string? excludeName = null)
|
||||
{
|
||||
var profile = GetById(id);
|
||||
if (profile != null)
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Profiles.Remove(profile);
|
||||
LastUpdated = DateTime.UtcNow;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot upgrade: assigns a stable id to every profile still missing one (Id == 0), in
|
||||
/// list order, and advances NextId past the highest id in use (self-healing a corrupt or
|
||||
/// legacy counter). Returns true when anything changed. Idempotent on subsequent calls.
|
||||
/// </summary>
|
||||
public bool EnsureIds()
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
var maxId = 0;
|
||||
foreach (var p in Profiles)
|
||||
// Check if name is already used (excluding the profile being renamed)
|
||||
var existing = GetProfile(name);
|
||||
if (existing != null && (excludeName == null || !existing.Name.Equals(excludeName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (p is not null && p.Id > maxId)
|
||||
{
|
||||
maxId = p.Id;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var next = NextId;
|
||||
if (next < 1)
|
||||
{
|
||||
next = 1;
|
||||
}
|
||||
|
||||
if (next <= maxId)
|
||||
{
|
||||
next = maxId + 1;
|
||||
}
|
||||
|
||||
foreach (var p in Profiles)
|
||||
{
|
||||
if (p is not null && p.Id == 0)
|
||||
{
|
||||
p.Id = next++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (NextId != next)
|
||||
{
|
||||
NextId = next;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,24 +121,6 @@ namespace PowerDisplay.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a profile by its stable id and persists to disk atomically.
|
||||
/// </summary>
|
||||
public static bool RemoveProfileById(int id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var profiles = LoadProfilesCore();
|
||||
bool removed = profiles.RemoveProfile(id);
|
||||
if (removed)
|
||||
{
|
||||
SaveProfilesCore(profiles);
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a profile by name.
|
||||
/// </summary>
|
||||
|
||||
@@ -377,12 +377,12 @@ namespace PowerDisplay
|
||||
}
|
||||
else if (messageType == Constants.PowerDisplayApplyProfileMessage())
|
||||
{
|
||||
// Apply profile by id
|
||||
if (messageParts.Length > 1 && _mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null
|
||||
&& int.TryParse(messageParts[1].Trim(), out var profileId))
|
||||
// Apply profile by name
|
||||
if (messageParts.Length > 1 && _mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
|
||||
{
|
||||
Logger.LogInfo($"[NamedPipe] Applying profile id: {profileId}");
|
||||
await mainWindow.ViewModel.ApplyProfileByIdAsync(profileId);
|
||||
var profileName = messageParts[1].Trim();
|
||||
Logger.LogInfo($"[NamedPipe] Applying profile: {profileName}");
|
||||
await mainWindow.ViewModel.ApplyProfileByNameAsync(profileName);
|
||||
}
|
||||
}
|
||||
else if (messageType == Constants.PowerDisplayTerminateAppMessage())
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure resolution + migration helpers for the LightSwitch theme->profile mapping. The mapping
|
||||
/// stores both a canonical profile id and a display/fallback name; resolution prefers the id.
|
||||
/// </summary>
|
||||
public static class LightSwitchProfileResolver
|
||||
{
|
||||
/// <summary>None sentinel that older/manual settings may hold in the name field.</summary>
|
||||
public const string NoneSentinel = "(None)";
|
||||
|
||||
/// <summary>
|
||||
/// Returns the effective profile id for a stored (id, name) pair, or null for "none":
|
||||
/// a positive id that still exists wins; otherwise a non-empty, non-"(None)" name resolves
|
||||
/// by first match; otherwise null. A stale/deleted id does NOT fall back to the name.
|
||||
/// </summary>
|
||||
public static int? Resolve(int storedId, string? storedName, PowerDisplayProfiles profiles)
|
||||
{
|
||||
if (profiles is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (storedId >= 1)
|
||||
{
|
||||
return profiles.GetById(storedId) is not null ? storedId : (int?)null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(storedName) && storedName != NoneSentinel)
|
||||
{
|
||||
var byName = profiles.GetProfile(storedName);
|
||||
if (byName is not null && byName.Id >= 1)
|
||||
{
|
||||
return byName.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot upgrade of a LightSwitch mapping from name-only to id+name. For each of light and
|
||||
/// dark: when the id is unset (0) and the stored name resolves to a profile, store its id and
|
||||
/// re-sync the name; when the name does not resolve, clear to "none" (id 0, empty name).
|
||||
/// Returns true when anything changed. Idempotent.
|
||||
/// </summary>
|
||||
public static bool MigrateNamesToIds(LightSwitchProperties props, PowerDisplayProfiles profiles)
|
||||
{
|
||||
if (props is null || profiles is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
changed |= MigrateOne(profiles, props.LightModeProfileId, props.LightModeProfile);
|
||||
changed |= MigrateOne(profiles, props.DarkModeProfileId, props.DarkModeProfile);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool MigrateOne(PowerDisplayProfiles profiles, IntProperty idProp, StringProperty nameProp)
|
||||
{
|
||||
if (idProp.Value >= 1)
|
||||
{
|
||||
return false; // already migrated
|
||||
}
|
||||
|
||||
var name = nameProp.Value;
|
||||
if (string.IsNullOrEmpty(name) || name == NoneSentinel)
|
||||
{
|
||||
return false; // already "none"
|
||||
}
|
||||
|
||||
var match = profiles.GetProfile(name);
|
||||
if (match is not null && match.Id >= 1)
|
||||
{
|
||||
idProp.Value = match.Id;
|
||||
nameProp.Value = match.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameProp.Value = string.Empty; // unknown/deleted -> none
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
using System;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using PowerDisplay.Common.Services;
|
||||
using Settings.UI.Library;
|
||||
|
||||
namespace PowerDisplay.Services
|
||||
@@ -19,11 +18,11 @@ namespace PowerDisplay.Services
|
||||
private const string LogPrefix = "[LightSwitch]";
|
||||
|
||||
/// <summary>
|
||||
/// Get the profile id to apply for the given theme.
|
||||
/// Get the profile name to apply for the given theme.
|
||||
/// </summary>
|
||||
/// <param name="isLightMode">Whether the theme changed to light mode.</param>
|
||||
/// <returns>The profile id to apply, or null if no profile is configured.</returns>
|
||||
public static int? GetProfileForTheme(bool isLightMode)
|
||||
/// <returns>The profile name to apply, or null if no profile is configured.</returns>
|
||||
public static string? GetProfileForTheme(bool isLightMode)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -37,37 +36,36 @@ namespace PowerDisplay.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
bool enabled;
|
||||
int storedId;
|
||||
string? storedName;
|
||||
string? profileName;
|
||||
if (isLightMode)
|
||||
{
|
||||
enabled = settings.Properties.EnableLightModeProfile.Value;
|
||||
storedId = settings.Properties.LightModeProfileId.Value;
|
||||
storedName = settings.Properties.LightModeProfile.Value;
|
||||
if (!settings.Properties.EnableLightModeProfile.Value)
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} Light mode profile is disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
profileName = settings.Properties.LightModeProfile.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
enabled = settings.Properties.EnableDarkModeProfile.Value;
|
||||
storedId = settings.Properties.DarkModeProfileId.Value;
|
||||
storedName = settings.Properties.DarkModeProfile.Value;
|
||||
if (!settings.Properties.EnableDarkModeProfile.Value)
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} Dark mode profile is disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
profileName = settings.Properties.DarkModeProfile.Value;
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
if (string.IsNullOrEmpty(profileName) || profileName == "(None)")
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} {(isLightMode ? "Light" : "Dark")} mode profile is disabled");
|
||||
Logger.LogInfo($"{LogPrefix} No profile configured for {(isLightMode ? "light" : "dark")} mode");
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = LightSwitchProfileResolver.Resolve(storedId, storedName, ProfileService.LoadProfiles());
|
||||
if (id is null)
|
||||
{
|
||||
Logger.LogInfo($"{LogPrefix} No profile resolved for {(isLightMode ? "light" : "dark")} mode");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"{LogPrefix} Profile id to apply: {id.Value}");
|
||||
return id;
|
||||
Logger.LogInfo($"{LogPrefix} Profile to apply: {profileName}");
|
||||
return profileName;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -154,31 +154,33 @@ public partial class MainViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply profile by id (called via Named Pipe from Settings UI). Preserves GUI behavior;
|
||||
/// only the lookup key changed from name to the stable id.
|
||||
/// Apply profile by name (called via Named Pipe from Settings UI)
|
||||
/// This is the new direct method that receives the profile name via IPC.
|
||||
/// </summary>
|
||||
/// <param name="profileId">The stable id of the profile to apply.</param>
|
||||
public async Task ApplyProfileByIdAsync(int profileId)
|
||||
/// <param name="profileName">The name of the profile to apply.</param>
|
||||
public async Task ApplyProfileByNameAsync(string profileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[Profile] Applying profile by id: {profileId}");
|
||||
Logger.LogInfo($"[Profile] Applying profile by name: {profileName}");
|
||||
|
||||
// Load profiles and find the requested one
|
||||
var profilesData = ProfileService.LoadProfiles();
|
||||
var profile = profilesData.GetById(profileId);
|
||||
var profile = profilesData.GetProfile(profileName);
|
||||
|
||||
if (profile == null || !profile.IsValid())
|
||||
{
|
||||
Logger.LogWarning($"[Profile] Profile id {profileId} not found or invalid");
|
||||
Logger.LogWarning($"[Profile] Profile '{profileName}' not found or invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the profile settings to monitors
|
||||
await ApplyProfileAsync(profile.MonitorSettings);
|
||||
Logger.LogInfo($"[Profile] Successfully applied profile id: {profileId}");
|
||||
Logger.LogInfo($"[Profile] Successfully applied profile: {profileName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[Profile] Failed to apply profile id {profileId}: {ex.Message}");
|
||||
Logger.LogError($"[Profile] Failed to apply profile '{profileName}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +191,9 @@ public partial class MainViewModel
|
||||
/// <param name="isLightMode">Whether the theme changed to light mode.</param>
|
||||
public void ApplyLightSwitchProfile(bool isLightMode)
|
||||
{
|
||||
var profileId = LightSwitchService.GetProfileForTheme(isLightMode);
|
||||
var profileName = LightSwitchService.GetProfileForTheme(isLightMode);
|
||||
|
||||
if (profileId is null)
|
||||
if (string.IsNullOrEmpty(profileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -200,15 +202,15 @@ public partial class MainViewModel
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[LightSwitch Integration] Applying profile id: {profileId.Value}");
|
||||
Logger.LogInfo($"[LightSwitch Integration] Applying profile: {profileName}");
|
||||
|
||||
// Load and apply the profile
|
||||
var profilesData = ProfileService.LoadProfiles();
|
||||
var profile = profilesData.GetById(profileId.Value);
|
||||
var profile = profilesData.GetProfile(profileName);
|
||||
|
||||
if (profile == null || !profile.IsValid())
|
||||
{
|
||||
Logger.LogWarning($"[LightSwitch Integration] Profile id {profileId.Value} not found or invalid");
|
||||
Logger.LogWarning($"[LightSwitch Integration] Profile '{profileName}' not found or invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,12 +260,6 @@ public partial class MainViewModel
|
||||
/// <summary>
|
||||
/// Apply profile settings to monitors. Profiles are per-monitor snapshots, so applying one
|
||||
/// turns off linked brightness before writing individual monitor values.
|
||||
/// <para>
|
||||
/// This method is the GUI code path. It is preserved exactly as-is: settings are dispatched
|
||||
/// in parallel via <c>Task.WhenAll</c> and no per-setting outcome is captured. All existing
|
||||
/// callers (<see cref="ApplyProfileByIdAsync"/>, <see cref="ApplyProfileAndCompleteAsync"/>)
|
||||
/// continue to call this overload.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private async Task ApplyProfileAsync(List<ProfileMonitorSetting> monitorSettings)
|
||||
{
|
||||
@@ -580,17 +576,6 @@ public partial class MainViewModel
|
||||
/// </summary>
|
||||
private void MigrateLegacyMonitorIdsInSideFiles()
|
||||
{
|
||||
// Assign stable ids to any pre-id profiles. Independent of monitor discovery, so it runs
|
||||
// before the "no monitors" early-return below.
|
||||
try
|
||||
{
|
||||
BackfillProfileIds();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[LegacyMigration] Failed to back-fill profile ids: {ex.Message}");
|
||||
}
|
||||
|
||||
var discovered = Monitors
|
||||
.Where(m => !string.IsNullOrEmpty(m.Id))
|
||||
.Select(m => (m.Id, m.MonitorNumber))
|
||||
@@ -671,36 +656,6 @@ public partial class MainViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private static void BackfillProfileIds()
|
||||
{
|
||||
var profiles = ProfileService.LoadProfiles();
|
||||
if (profiles is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (profiles.EnsureIds())
|
||||
{
|
||||
ProfileService.SaveProfiles(profiles);
|
||||
Logger.LogInfo("[LegacyMigration] profiles.json updated with stable profile ids.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var lightSwitch = SettingsUtils.Default.GetSettingsOrDefault<LightSwitchSettings>(LightSwitchSettings.ModuleName);
|
||||
if (lightSwitch?.Properties != null
|
||||
&& LightSwitchProfileResolver.MigrateNamesToIds(lightSwitch.Properties, profiles))
|
||||
{
|
||||
SettingsUtils.Default.SaveSettings(lightSwitch.ToJsonString(), LightSwitchSettings.ModuleName);
|
||||
Logger.LogInfo("[LegacyMigration] LightSwitch profile references migrated to ids.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[LegacyMigration] Failed to migrate LightSwitch profile references: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal Settings UI that the monitor list has been refreshed
|
||||
/// </summary>
|
||||
|
||||
@@ -21,8 +21,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public const bool DefaultEnableLightModeProfile = false;
|
||||
public const string DefaultDarkModeProfile = "";
|
||||
public const string DefaultLightModeProfile = "";
|
||||
public const int DefaultDarkModeProfileId = 0;
|
||||
public const int DefaultLightModeProfileId = 0;
|
||||
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
|
||||
|
||||
public LightSwitchProperties()
|
||||
@@ -41,8 +39,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
EnableLightModeProfile = new BoolProperty(DefaultEnableLightModeProfile);
|
||||
DarkModeProfile = new StringProperty(DefaultDarkModeProfile);
|
||||
LightModeProfile = new StringProperty(DefaultLightModeProfile);
|
||||
DarkModeProfileId = new IntProperty(DefaultDarkModeProfileId);
|
||||
LightModeProfileId = new IntProperty(DefaultLightModeProfileId);
|
||||
}
|
||||
|
||||
[JsonPropertyName("changeSystem")]
|
||||
@@ -86,11 +82,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("lightModeProfile")]
|
||||
public StringProperty LightModeProfile { get; set; }
|
||||
|
||||
[JsonPropertyName("darkModeProfileId")]
|
||||
public IntProperty DarkModeProfileId { get; set; }
|
||||
|
||||
[JsonPropertyName("lightModeProfileId")]
|
||||
public IntProperty LightModeProfileId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
EnableLightModeProfile = new BoolProperty(Properties.EnableLightModeProfile.Value),
|
||||
DarkModeProfile = new StringProperty(Properties.DarkModeProfile.Value),
|
||||
LightModeProfile = new StringProperty(Properties.LightModeProfile.Value),
|
||||
DarkModeProfileId = new IntProperty(Properties.DarkModeProfileId.Value),
|
||||
LightModeProfileId = new IntProperty(Properties.LightModeProfileId.Value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
</tkcontrols:SettingsCard.Header>
|
||||
<ComboBox
|
||||
MinWidth="200"
|
||||
DisplayMemberPath="DisplayName"
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableProfiles, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedDarkModeProfile, Mode=TwoWay}" />
|
||||
@@ -259,7 +259,7 @@
|
||||
</tkcontrols:SettingsCard.Header>
|
||||
<ComboBox
|
||||
MinWidth="200"
|
||||
DisplayMemberPath="DisplayName"
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableProfiles, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedLightModeProfile, Mode=TwoWay}" />
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander.ItemTemplate>
|
||||
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">
|
||||
<tkcontrols:SettingsCard Header="{x:Bind DisplayName}">
|
||||
<tkcontrols:SettingsCard Header="{x:Bind Name}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayActionButtonSpacing}">
|
||||
<Button
|
||||
x:Uid="PowerDisplay_Profile_ApplyButton"
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
if (result == ContentDialogResult.Primary && dialog.ResultProfile != null)
|
||||
{
|
||||
ViewModel.UpdateProfile(dialog.ResultProfile);
|
||||
ViewModel.UpdateProfile(profile.Name, dialog.ResultProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
ViewModel.DeleteProfile(profile.Id);
|
||||
ViewModel.DeleteProfile(profile.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
// Set profile name
|
||||
ViewModel.ProfileName = profile.Name;
|
||||
|
||||
// Carry the edited profile's stable id so saving replaces it in place (a rename keeps the id).
|
||||
ViewModel.EditingId = profile.Id;
|
||||
|
||||
// Pre-fill monitor settings from existing profile
|
||||
foreach (var monitorSetting in profile.MonitorSettings)
|
||||
{
|
||||
|
||||
@@ -650,14 +650,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
_selectedDarkModeProfile = value;
|
||||
|
||||
// Sync both the canonical id and the display/fallback name into settings.
|
||||
var newId = value?.Id ?? 0;
|
||||
var newName = value?.Name ?? string.Empty;
|
||||
if (ModuleSettings.Properties.DarkModeProfileId.Value != newId
|
||||
|| ModuleSettings.Properties.DarkModeProfile.Value != newName)
|
||||
// Sync with the string property stored in settings
|
||||
var newProfileName = value?.Name ?? string.Empty;
|
||||
if (ModuleSettings.Properties.DarkModeProfile.Value != newProfileName)
|
||||
{
|
||||
ModuleSettings.Properties.DarkModeProfileId.Value = newId;
|
||||
ModuleSettings.Properties.DarkModeProfile.Value = newName;
|
||||
ModuleSettings.Properties.DarkModeProfile.Value = newProfileName;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
@@ -675,14 +672,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
_selectedLightModeProfile = value;
|
||||
|
||||
// Sync both the canonical id and the display/fallback name into settings.
|
||||
var newId = value?.Id ?? 0;
|
||||
var newName = value?.Name ?? string.Empty;
|
||||
if (ModuleSettings.Properties.LightModeProfileId.Value != newId
|
||||
|| ModuleSettings.Properties.LightModeProfile.Value != newName)
|
||||
// Sync with the string property stored in settings
|
||||
var newProfileName = value?.Name ?? string.Empty;
|
||||
if (ModuleSettings.Properties.LightModeProfile.Value != newProfileName)
|
||||
{
|
||||
ModuleSettings.Properties.LightModeProfileId.Value = newId;
|
||||
ModuleSettings.Properties.LightModeProfile.Value = newName;
|
||||
ModuleSettings.Properties.LightModeProfile.Value = newProfileName;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
@@ -702,7 +696,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
ModuleSettings.Properties.DarkModeProfile.Value = value;
|
||||
|
||||
// Sync with the object property
|
||||
SelectByStoredReference(isDarkMode: true);
|
||||
UpdateSelectedProfileFromName(value, isDarkMode: true);
|
||||
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
@@ -719,7 +713,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
ModuleSettings.Properties.LightModeProfile.Value = value;
|
||||
|
||||
// Sync with the object property
|
||||
SelectByStoredReference(isDarkMode: false);
|
||||
UpdateSelectedProfileFromName(value, isDarkMode: false);
|
||||
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
@@ -741,9 +735,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
Logger.LogInfo($"Loaded {profilesData.Profiles.Count} PowerDisplay profiles");
|
||||
|
||||
// Sync selected profiles from settings (id-first, name fallback).
|
||||
SelectByStoredReference(isDarkMode: true);
|
||||
SelectByStoredReference(isDarkMode: false);
|
||||
// Sync selected profiles from settings
|
||||
UpdateSelectedProfileFromName(ModuleSettings.Properties.DarkModeProfile.Value, isDarkMode: true);
|
||||
UpdateSelectedProfileFromName(ModuleSettings.Properties.LightModeProfile.Value, isDarkMode: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -753,43 +747,49 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the profile object for the given theme from settings, preferring the stable id and
|
||||
/// falling back to the stored name only when no id is set. A stored id that no longer matches
|
||||
/// any profile clears the selection (it does NOT fall back to the name), mirroring the
|
||||
/// app-side <c>LightSwitchProfileResolver.Resolve</c>.
|
||||
/// Helper method to sync the selected profile object from the profile name stored in settings.
|
||||
/// If the configured profile no longer exists, clears the selection and updates settings.
|
||||
/// </summary>
|
||||
private void SelectByStoredReference(bool isDarkMode)
|
||||
private void UpdateSelectedProfileFromName(string profileName, bool isDarkMode)
|
||||
{
|
||||
var storedId = isDarkMode
|
||||
? ModuleSettings.Properties.DarkModeProfileId.Value
|
||||
: ModuleSettings.Properties.LightModeProfileId.Value;
|
||||
var storedName = isDarkMode
|
||||
? ModuleSettings.Properties.DarkModeProfile.Value
|
||||
: ModuleSettings.Properties.LightModeProfile.Value;
|
||||
PowerDisplayProfile? matchingProfile = null;
|
||||
|
||||
PowerDisplayProfile? match = null;
|
||||
if (storedId >= 1)
|
||||
if (!string.IsNullOrEmpty(profileName))
|
||||
{
|
||||
match = AvailableProfiles.FirstOrDefault(p => p.Id == storedId);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(storedName) && storedName != "(None)")
|
||||
{
|
||||
match = AvailableProfiles.FirstOrDefault(p => p.Name.Equals(storedName, StringComparison.OrdinalIgnoreCase));
|
||||
matchingProfile = AvailableProfiles.FirstOrDefault(p =>
|
||||
p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// If the configured profile no longer exists, clear it from settings
|
||||
if (matchingProfile == null)
|
||||
{
|
||||
Logger.LogWarning($"Configured {(isDarkMode ? "dark" : "light")} mode profile '{profileName}' no longer exists, clearing selection");
|
||||
|
||||
if (isDarkMode)
|
||||
{
|
||||
ModuleSettings.Properties.DarkModeProfile.Value = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
ModuleSettings.Properties.LightModeProfile.Value = string.Empty;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
if (isDarkMode)
|
||||
{
|
||||
if (_selectedDarkModeProfile != match)
|
||||
if (_selectedDarkModeProfile != matchingProfile)
|
||||
{
|
||||
_selectedDarkModeProfile = match;
|
||||
_selectedDarkModeProfile = matchingProfile;
|
||||
NotifyPropertyChanged(nameof(SelectedDarkModeProfile));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedLightModeProfile != match)
|
||||
if (_selectedLightModeProfile != matchingProfile)
|
||||
{
|
||||
_selectedLightModeProfile = match;
|
||||
_selectedLightModeProfile = matchingProfile;
|
||||
NotifyPropertyChanged(nameof(SelectedLightModeProfile));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,10 +803,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Applying profile: {profile.DisplayName}");
|
||||
Logger.LogInfo($"Applying profile: {profile.Name}");
|
||||
|
||||
// Send custom action to trigger profile application
|
||||
// The profile id is passed via Named Pipe IPC to PowerDisplay.exe
|
||||
// The profile name is passed via Named Pipe IPC to PowerDisplay.exe
|
||||
var actionMessage = new PowerDisplayActionMessage
|
||||
{
|
||||
Action = new PowerDisplayActionMessage.ActionData
|
||||
@@ -814,14 +814,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
PowerDisplay = new PowerDisplayActionMessage.PowerDisplayAction
|
||||
{
|
||||
ActionName = "ApplyProfile",
|
||||
Value = profile.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
Value = profile.Name,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
SendConfigMSG(JsonSerializer.Serialize(actionMessage, SettingsSerializationContext.Default.PowerDisplayActionMessage));
|
||||
|
||||
Logger.LogInfo($"Profile '{profile.DisplayName}' apply request sent via IPC");
|
||||
Logger.LogInfo($"Profile '{profile.Name}' apply request sent via IPC");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -861,22 +861,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing profile. The profile's stable id is the key, so this replaces the
|
||||
/// matching entry in place (a rename keeps the same id).
|
||||
/// Update an existing profile
|
||||
/// </summary>
|
||||
public void UpdateProfile(PowerDisplayProfile updatedProfile)
|
||||
public void UpdateProfile(string oldName, PowerDisplayProfile newProfile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (updatedProfile == null || !updatedProfile.IsValid())
|
||||
if (newProfile == null || !newProfile.IsValid())
|
||||
{
|
||||
Logger.LogWarning("Invalid profile");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Updating profile: {updatedProfile.DisplayName}");
|
||||
Logger.LogInfo($"Updating profile: {oldName} -> {newProfile.Name}");
|
||||
|
||||
ProfileHelper.AddOrUpdateProfile(updatedProfile);
|
||||
ProfileHelper.RenameAndUpdateProfile(oldName, newProfile);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
@@ -884,7 +883,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Signal PowerDisplay to reload profiles
|
||||
SignalSettingsUpdated();
|
||||
|
||||
Logger.LogInfo($"Profile '{updatedProfile.DisplayName}' updated successfully");
|
||||
Logger.LogInfo($"Profile updated to '{newProfile.Name}' successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -893,20 +892,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a profile by its stable id.
|
||||
/// Delete a profile
|
||||
/// </summary>
|
||||
public void DeleteProfile(int id)
|
||||
public void DeleteProfile(string profileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (id < 1)
|
||||
if (string.IsNullOrEmpty(profileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Deleting profile id: {id}");
|
||||
Logger.LogInfo($"Deleting profile: {profileName}");
|
||||
|
||||
ProfileHelper.RemoveProfileById(id);
|
||||
ProfileHelper.RemoveProfile(profileName);
|
||||
|
||||
// Reload profile list
|
||||
LoadProfiles();
|
||||
@@ -914,7 +913,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Signal PowerDisplay to reload profiles
|
||||
SignalSettingsUpdated();
|
||||
|
||||
Logger.LogInfo($"Profile id {id} deleted successfully");
|
||||
Logger.LogInfo($"Profile '{profileName}' deleted successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -70,13 +70,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stable id of the profile being edited. 0 for a brand-new profile
|
||||
/// (the store assigns a fresh id on save); the existing id when editing, so the update
|
||||
/// replaces the same profile in place.
|
||||
/// </summary>
|
||||
public int EditingId { get; set; }
|
||||
|
||||
public ObservableCollection<MonitorSelectionItem> Monitors
|
||||
{
|
||||
get => _monitors;
|
||||
@@ -110,7 +103,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
m.IncludeVolume && m.SupportsVolume ? (int?)m.Volume : null))
|
||||
.ToList();
|
||||
|
||||
return new PowerDisplayProfile(_profileName, settings) { Id = EditingId };
|
||||
return new PowerDisplayProfile(_profileName, settings);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
Reference in New Issue
Block a user