Compare commits

...

7 Commits

Author SHA1 Message Date
Michael Jolley
000f0af4fb refactor: remove DispatcherQueue, reinstate OpenUrlCommand, fix Dispose pattern
- Remove Microsoft.UI.Dispatching dependency from extension; the
  DockBandViewModel already marshals to the UI thread via DoOnUiThread
  in HandleItemsChanged, matching the PerformanceMonitor pattern.
- Reinstate OpenUrlCommand("ms-actioncenter:") on NowDockBand so
  clicking the clock still opens Notification Center (was regressed
  to NoOpCommand).
- Replace #pragma CA1816 suppression with proper GC.SuppressFinalize(this)
  in Dispose, matching PerformanceMonitorCommandsProvider pattern.
- Make _copyTimeCommand/_copyDateCommand private with internal property
  accessors to satisfy SA1401.
- Enable <Nullable>enable</Nullable> project-wide in both the extension
  and test csproj files, fixing all resulting nullable annotations
  throughout the codebase.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 23:20:51 -05:00
Michael Jolley
cfca046f9f Merge branch 'main' into dev/mjolley/fix-dock-clock-stale-seconds 2026-06-18 19:15:45 -05:00
Hawk
9fd573b71e merge: resolve conflicts with main 2026-06-18 16:21:49 -05:00
Hawk
42c5ce436f fix: address review for #48253 — dispatcher marshalling for dock band updates and provider disposal 2026-06-18 14:20:08 -05:00
root
89e0e73a0c test: add NowDockBandTests and GetDockBands_ReturnsNonEmptyArray for issue #46192
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 22:46:17 -05:00
root
c18a4dc367 Fix dock clock: extract NowDockBand, add seconds, fix stale refresh via ItemsChanged callback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 22:39:54 -05:00
root
776a0f6b0d a11y: suppress live-region AT announcements on DockItemControl for per-second clock updates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 22:33:13 -05:00
19 changed files with 305 additions and 128 deletions

View File

@@ -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}" />

View File

@@ -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()

View File

@@ -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()

View File

@@ -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>

View File

@@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using 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:32 PM", _band.Title);
Assert.IsFalse(string.IsNullOrEmpty(_band.Subtitle));
}
[TestMethod]
public void UpdateText_LongTimeFormat_TitleContainsSeconds()
{
_band = new NowDockBand(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}'");
}
}

View File

@@ -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]

View File

@@ -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

View File

@@ -22,7 +22,7 @@ public class Settings : ISettingsInterface
bool enableFallbackItems = true,
bool timeWithSecond = false,
bool dateWithWeekday = false,
List<string> customFormats = null)
List<string>? customFormats = null)
{
this.firstWeekOfYear = firstWeekOfYear;
this.firstDayOfWeek = firstDayOfWeek;

View File

@@ -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()

View File

@@ -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()

View File

@@ -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);

View File

@@ -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,16 @@ 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");
}
}
}

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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

View File

@@ -145,7 +145,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
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()
{

View File

@@ -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>

View File

@@ -0,0 +1,74 @@
// 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 readonly System.Timers.Timer _timer;
private readonly Action? _onUpdated;
private readonly Func<DateTime> _clock;
private CopyTextCommand _copyTimeCommand;
private CopyTextCommand _copyDateCommand;
internal CopyTextCommand CopyTimeCommand => _copyTimeCommand;
internal CopyTextCommand CopyDateCommand => _copyDateCommand;
internal NowDockBand(Action? onUpdated = null, Func<DateTime>? clock = null)
{
_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(1000) { AutoReset = true };
_timer.Elapsed += (_, _) => UpdateText();
_timer.Start();
}
internal void UpdateText()
{
var now = _clock();
var timeString = now.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, true, 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();
}
}

View File

@@ -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,39 @@ 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(onUpdated: () =>
{
if (wrappedBand is not null)
{
wrappedBand.Items = [_nowDockBand!];
}
});
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)
{
Icon = Icons.NotificationCenterIcon,
};
}
private string GetTranslatedPluginDescription()
@@ -56,97 +90,20 @@ 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,
};
return new ICommandItem[] { clockBand, notificationBand };
public override void Dispose()
{
_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()