mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-08 03:20:50 +02:00
Compare commits
4 Commits
yuleng/m/c
...
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()
|
||||
|
||||
Reference in New Issue
Block a user