diff --git a/DesktopClock.Tests/SystemClockTimerTests.cs b/DesktopClock.Tests/SystemClockTimerTests.cs
index 22da451..4128113 100644
--- a/DesktopClock.Tests/SystemClockTimerTests.cs
+++ b/DesktopClock.Tests/SystemClockTimerTests.cs
@@ -19,7 +19,7 @@ public void Countdown_ReadsWholeSecondsWhereverTheTickLands(string now, string e
{
var second = TickAt(now);
- var result = TimeStringFormatter.Format(second, second.DateTime, TimeZoneInfo.Utc, Target.DateTime, null, "{mm}:{ss}", CultureInfo.InvariantCulture);
+ var result = TimeStringFormatter.Format(second, TimeZoneInfo.Utc, Target.DateTime, null, "{mm}:{ss}", CultureInfo.InvariantCulture);
Assert.Equal(expected, result);
}
@@ -32,7 +32,7 @@ public void DefaultCountdown_ReadsWholeSecondsWhereverTheTickLands(string now, s
{
var second = TickAt(now);
- var result = TimeStringFormatter.Format(second, second.DateTime, TimeZoneInfo.Utc, Target.DateTime, null, null, CultureInfo.InvariantCulture);
+ var result = TimeStringFormatter.Format(second, TimeZoneInfo.Utc, Target.DateTime, null, null, CultureInfo.InvariantCulture);
Assert.Equal(expected, result);
}
@@ -43,7 +43,7 @@ public void Countdown_StandardFormatHasNoFractionOfASecond()
// Issue #106: the "t" format showed values like 00:04:58.9870000.
var second = TickAt("12:00:01.013");
- var result = TimeStringFormatter.Format(second, second.DateTime, TimeZoneInfo.Utc, Target.DateTime, null, "t", CultureInfo.InvariantCulture);
+ var result = TimeStringFormatter.Format(second, TimeZoneInfo.Utc, Target.DateTime, null, "t", CultureInfo.InvariantCulture);
Assert.Equal("00:04:59", result);
}
diff --git a/DesktopClock.Tests/TimeStringFormatterTests.cs b/DesktopClock.Tests/TimeStringFormatterTests.cs
index 2cff299..dd8ca3a 100644
--- a/DesktopClock.Tests/TimeStringFormatterTests.cs
+++ b/DesktopClock.Tests/TimeStringFormatterTests.cs
@@ -14,7 +14,6 @@ public void Format_UsesFormatWhenNoCountdown()
var result = TimeStringFormatter.Format(
now,
- now.DateTime,
timeZone,
default,
"HH:mm",
@@ -32,7 +31,6 @@ public void Format_UsesFormatProviderForStandardFormats()
var result = TimeStringFormatter.Format(
now,
- now.DateTime,
TimeZoneInfo.Utc,
default,
"d",
@@ -51,7 +49,6 @@ public void Format_UsesTokenizedCountdownFormat()
var result = TimeStringFormatter.Format(
now,
- nowDateTime,
TimeZoneInfo.Utc,
countdownTo,
"HH:mm",
@@ -71,7 +68,6 @@ public void Format_UsesCountdownFormatWhenProvided()
var result = TimeStringFormatter.Format(
now,
- nowDateTime,
TimeZoneInfo.Utc,
countdownTo,
"HH:mm",
@@ -90,7 +86,6 @@ public void Format_UsesRelativePhraseWhenCountdownFormatMissing()
var result = TimeStringFormatter.Format(
now,
- nowDateTime,
TimeZoneInfo.Utc,
countdownTo,
"HH:mm",
@@ -115,7 +110,6 @@ public void Format_HumanizedCountdownIsRelativeToNow(int hoursFromNow, string ex
var result = TimeStringFormatter.Format(
now,
- now.DateTime,
TimeZoneInfo.Utc,
countdownTo,
"HH:mm",
@@ -133,7 +127,6 @@ public void Format_ConvertsUsingSelectedTimeZone()
var result = TimeStringFormatter.Format(
now,
- now.DateTime,
timeZone,
default,
"HH:mm",
@@ -152,7 +145,6 @@ public void Format_WeekTokenUsesSelectedTimeZone()
var result = TimeStringFormatter.Format(
now,
- now.DateTime,
timeZone,
default,
"{weekYear}-W{week}",
@@ -169,7 +161,6 @@ public void Format_InvalidClockFormatShowsBriefErrorMessage()
var result = TimeStringFormatter.Format(
now,
- now.DateTime,
TimeZoneInfo.Utc,
default,
"{Q}",
@@ -187,7 +178,6 @@ public void Format_InvalidCountdownFormatShowsBriefErrorMessage()
var result = TimeStringFormatter.Format(
now,
- nowDateTime,
TimeZoneInfo.Utc,
nowDateTime.AddMinutes(30),
"HH:mm",
diff --git a/DesktopClock/App.xaml.cs b/DesktopClock/App.xaml.cs
index 3a8f2d4..fa088a3 100644
--- a/DesktopClock/App.xaml.cs
+++ b/DesktopClock/App.xaml.cs
@@ -103,9 +103,6 @@ public static void SetRunOnStartup(bool runOnStartup)
private static string GetSha256Hash(string text)
{
- if (string.IsNullOrEmpty(text))
- return string.Empty;
-
using var sha = new System.Security.Cryptography.SHA256Managed();
var textData = System.Text.Encoding.UTF8.GetBytes(text);
var hash = sha.ComputeHash(textData);
diff --git a/DesktopClock/CountdownTargetEditor.xaml.cs b/DesktopClock/CountdownTargetEditor.xaml.cs
index 93b2d5f..cf038a7 100644
--- a/DesktopClock/CountdownTargetEditor.xaml.cs
+++ b/DesktopClock/CountdownTargetEditor.xaml.cs
@@ -2,7 +2,6 @@
using System.ComponentModel;
using System.Globalization;
using System.Windows;
-using System.Windows.Automation;
using System.Windows.Controls;
using DesktopClock.Properties;
@@ -57,37 +56,10 @@ private void BuildPresetButtons()
{
foreach (var (name, getTarget) in Presets)
{
- var title = new TextBlock
- {
- Text = name,
- FontWeight = FontWeights.SemiBold,
- };
-
- var example = new TextBlock
- {
- Text = getTarget().ToString(FormatPresets.ShortDateTimePattern(CultureInfo.CurrentCulture)),
- FontSize = 12,
- TextTrimming = TextTrimming.CharacterEllipsis,
- };
- example.SetResourceReference(ForegroundProperty, "TextSecondaryBrush");
-
- var content = new StackPanel();
- content.Children.Add(title);
- content.Children.Add(example);
-
- var button = new Button
- {
- Content = content,
- MaxWidth = 280,
- HorizontalContentAlignment = HorizontalAlignment.Left,
- Margin = new Thickness(0, 0, 8, 4),
- };
- AutomationProperties.SetName(button, name);
+ var example = getTarget().ToString(FormatPresets.ShortDateTimePattern(CultureInfo.CurrentCulture));
// Saved as a wall-clock time like a typed date, since a local one is saved with its UTC offset and would move after a time zone change.
- button.Click += (_, _) => Settings.Default.CountdownTo = DateTime.SpecifyKind(getTarget(), DateTimeKind.Unspecified);
-
- PresetsPanel.Children.Add(button);
+ PresetButton.Add(PresetsPanel, name, example, () => Settings.Default.CountdownTo = DateTime.SpecifyKind(getTarget(), DateTimeKind.Unspecified));
}
}
diff --git a/DesktopClock/FormatEditor.xaml.cs b/DesktopClock/FormatEditor.xaml.cs
index 3eafd4c..c022e0b 100644
--- a/DesktopClock/FormatEditor.xaml.cs
+++ b/DesktopClock/FormatEditor.xaml.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
-using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
@@ -110,37 +109,9 @@ private void BuildPresetButtons()
foreach (var (name, format) in presets)
{
- var title = new TextBlock
- {
- Text = name,
- FontWeight = FontWeights.SemiBold,
- };
-
- var example = new TextBlock
- {
- Text = FormatPreview(format),
- FontSize = 12,
- TextTrimming = TextTrimming.CharacterEllipsis,
- };
- example.SetResourceReference(ForegroundProperty, "TextSecondaryBrush");
+ var toolTip = string.IsNullOrEmpty(format) ? Loc.Get("AutomaticTooltip") : format;
+ var example = PresetButton.Add(PresetsPanel, name, FormatPreview(format), () => SetFormat(format), toolTip);
_presetExamples.Add((example, format));
-
- var content = new StackPanel();
- content.Children.Add(title);
- content.Children.Add(example);
-
- var button = new Button
- {
- Content = content,
- MaxWidth = 280,
- HorizontalContentAlignment = HorizontalAlignment.Left,
- Margin = new Thickness(0, 0, 8, 4),
- ToolTip = string.IsNullOrEmpty(format) ? Loc.Get("AutomaticTooltip") : format,
- };
- AutomationProperties.SetName(button, name);
- button.Click += (_, _) => SetFormat(format);
-
- PresetsPanel.Children.Add(button);
}
}
@@ -220,7 +191,7 @@ private string FormatPreview(string format)
if (Mode == FormatEditorMode.Clock)
{
- return TimeStringFormatter.Format(now, now.DateTime, timeZone, default, format, string.Empty, culture);
+ return TimeStringFormatter.Format(now, timeZone, default, format, string.Empty, culture);
}
// Preview against the real countdown target, or a sample one when none is set.
@@ -228,6 +199,6 @@ private string FormatPreview(string format)
Settings.Default.CountdownTo :
now.DateTime.AddDays(3).AddHours(4).AddMinutes(30).AddSeconds(10);
- return TimeStringFormatter.Format(now, now.DateTime, timeZone, countdownTo, string.Empty, format, culture);
+ return TimeStringFormatter.Format(now, timeZone, countdownTo, string.Empty, format, culture);
}
}
diff --git a/DesktopClock/MainWindow.xaml.cs b/DesktopClock/MainWindow.xaml.cs
index 7c72ba8..ebeaa81 100644
--- a/DesktopClock/MainWindow.xaml.cs
+++ b/DesktopClock/MainWindow.xaml.cs
@@ -37,7 +37,6 @@ public partial class MainWindow : Window
private FullscreenHideManager _fullscreenHideManager;
private DesktopPinner _desktopPinner;
private readonly WheelNotches _wheelNotches = new();
- private readonly PropertyChangedEventHandler _settingsPropertyChanged;
///
/// The current date and time in the selected time zone, or the formatted countdown text.
@@ -52,8 +51,7 @@ public MainWindow()
_timeZone = Settings.Default.TimeZoneInfo;
- _settingsPropertyChanged = (s, e) => Dispatcher.Invoke(() => Settings_PropertyChanged(s, e));
- Settings.Default.PropertyChanged += _settingsPropertyChanged;
+ Settings.Default.PropertyChanged += Settings_PropertyChanged;
ApplyWindowVisibilitySettings();
@@ -137,9 +135,6 @@ private void SystemEvents_TimeChanged(object sender, EventArgs e)
///
private void ThemeManager_ThemeChanged(object sender, EventArgs e)
{
- if (_trayIcon == null)
- return;
-
// A new instance (x:Shared="False") resolves its DynamicResource colors against the current palette when it next opens.
var menu = Resources["MainContextMenu"] as ContextMenu;
menu.DataContext = this;
@@ -231,7 +226,7 @@ public void Exit()
protected override void OnClosed(EventArgs e)
{
- Settings.Default.PropertyChanged -= _settingsPropertyChanged;
+ Settings.Default.PropertyChanged -= Settings_PropertyChanged;
ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
SystemEvents.TimeChanged -= SystemEvents_TimeChanged;
SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;
@@ -239,7 +234,6 @@ protected override void OnClosed(EventArgs e)
_systemClockTimer.SecondChanged -= SystemClockTimer_SecondChanged;
_systemClockTimer.Dispose();
- _trayIcon?.Dispose();
_soundPlayer?.Dispose();
_desktopPinner?.Dispose();
@@ -254,23 +248,20 @@ protected override void OnActivated(EventArgs e)
private void ConfigureTrayIcon()
{
- if (_trayIcon == null)
+ // Construct the tray icon from the XAML resources.
+ _trayIcon = Resources["TrayIcon"] as TaskbarIcon;
+ _trayIcon.ContextMenu = Resources["MainContextMenu"] as ContextMenu;
+ _trayIcon.ContextMenu.DataContext = this;
+ try
{
- // Construct the tray icon from the XAML resources.
- _trayIcon = Resources["TrayIcon"] as TaskbarIcon;
- _trayIcon.ContextMenu = Resources["MainContextMenu"] as ContextMenu;
- _trayIcon.ContextMenu.DataContext = this;
- try
- {
- _trayIcon.ForceCreate(enablesEfficiencyMode: false);
- }
- catch (InvalidOperationException)
- {
- // The taskbar isn't there yet, such as early in sign-in, and the icon is added once it starts.
- }
-
- _trayIcon.TrayLeftMouseDoubleClick += (_, _) => ShowClock();
+ _trayIcon.ForceCreate(enablesEfficiencyMode: false);
+ }
+ catch (InvalidOperationException)
+ {
+ // The taskbar isn't there yet, such as early in sign-in, and the icon is added once it starts.
}
+
+ _trayIcon.TrayLeftMouseDoubleClick += (_, _) => ShowClock();
}
///
@@ -460,7 +451,6 @@ private void UpdateTimeString(DateTimeOffset second)
{
CurrentTimeOrCountdownString = TimeStringFormatter.Format(
second,
- second.DateTime,
_timeZone,
Settings.Default.CountdownTo,
Settings.Default.Format,
@@ -563,13 +553,10 @@ private void Window_Closing(object sender, CancelEventArgs e)
Settings.Default.LastDisplay = CurrentTimeOrCountdownString;
Settings.Default.Placement = this.GetPlacement();
- // Stop the file watcher before saving.
- Settings.Default.Dispose();
-
if (Settings.CanBeSaved)
Settings.Default.Save();
- _trayIcon?.Dispose();
+ _trayIcon.Dispose();
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
diff --git a/DesktopClock/OutlinedTextBlock.cs b/DesktopClock/OutlinedTextBlock.cs
index ac87ab0..558da74 100644
--- a/DesktopClock/OutlinedTextBlock.cs
+++ b/DesktopClock/OutlinedTextBlock.cs
@@ -14,7 +14,8 @@ public class OutlinedTextBlock : FrameworkElement
{
private void UpdatePen()
{
- _Pen = new Pen(Stroke, StrokeThickness)
+ // Twice as thick, since the outline is clipped to outside the text and the inner half is hidden.
+ _Pen = new Pen(Stroke, StrokeThickness * 2)
{
DashCap = PenLineCap.Round,
EndLineCap = PenLineCap.Round,
@@ -22,26 +23,9 @@ private void UpdatePen()
StartLineCap = PenLineCap.Round
};
- if (StrokePosition is StrokePosition.Outside or StrokePosition.Inside)
- {
- _Pen.Thickness = StrokeThickness * 2;
- }
-
InvalidateVisual();
}
- public StrokePosition StrokePosition
- {
- get => (StrokePosition)GetValue(StrokePositionProperty);
- set => SetValue(StrokePositionProperty, value);
- }
-
- public static readonly DependencyProperty StrokePositionProperty =
- DependencyProperty.Register("StrokePosition",
- typeof(StrokePosition),
- typeof(OutlinedTextBlock),
- new FrameworkPropertyMetadata(StrokePosition.Outside, FrameworkPropertyMetadataOptions.AffectsRender));
-
public static readonly DependencyProperty FillProperty = DependencyProperty.Register(
"Fill",
typeof(Brush),
@@ -212,22 +196,10 @@ protected override void OnRender(DrawingContext drawingContext)
return;
}
- if (StrokePosition == StrokePosition.Outside)
- {
- _clipGeometry ??= BuildOutsideClipGeometry();
- drawingContext.PushClip(_clipGeometry);
- }
- else if (StrokePosition == StrokePosition.Inside)
- {
- drawingContext.PushClip(_TextGeometry);
- }
-
+ _clipGeometry ??= BuildOutsideClipGeometry();
+ drawingContext.PushClip(_clipGeometry);
drawingContext.DrawGeometry(null, _Pen, _TextGeometry);
-
- if (StrokePosition is StrokePosition.Outside or StrokePosition.Inside)
- {
- drawingContext.Pop();
- }
+ drawingContext.Pop();
}
protected override Size MeasureOverride(Size availableSize)
@@ -341,15 +313,7 @@ private bool HasVisibleStroke() =>
private Geometry BuildOutsideClipGeometry()
{
// https://stackoverflow.com/questions/93650/apply-stroke-to-a-textblock-in-wpf/35262509#comment106994536_49636033.
- //var boundsGeo = new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight));
var boundsGeo = new RectangleGeometry(new Rect(-(2 * StrokeThickness), -(2 * StrokeThickness), ActualWidth + (4 * StrokeThickness), ActualHeight + (4 * StrokeThickness)));
return Geometry.Combine(boundsGeo, _TextGeometry, GeometryCombineMode.Exclude, null);
}
}
-
-public enum StrokePosition
-{
- Center,
- Outside,
- Inside
-}
diff --git a/DesktopClock/Properties/Settings.cs b/DesktopClock/Properties/Settings.cs
index cf3ae9c..494b53f 100644
--- a/DesktopClock/Properties/Settings.cs
+++ b/DesktopClock/Properties/Settings.cs
@@ -11,7 +11,7 @@
namespace DesktopClock.Properties;
-public sealed class Settings : INotifyPropertyChanged, IDisposable
+public sealed class Settings : INotifyPropertyChanged
{
private readonly FileSystemWatcher _watcher;
private readonly DispatcherTimer _saveTimer;
@@ -525,7 +525,7 @@ public bool Save()
}
catch
{
- // Wait before next attempt to read.
+ // Wait before trying again.
System.Threading.Thread.Sleep(250);
}
}
@@ -678,10 +678,4 @@ private void ApplySystemThemeDefaultsIfAvailable()
TextColor = textColor;
OuterColor = outerColor;
}
-
- public void Dispose()
- {
- // We don't dispose of the watcher anymore because it would actually hang indefinitely if you had multiple instances of the same clock open.
- //_watcher?.Dispose();
- }
}
diff --git a/DesktopClock/SettingsWindow.xaml.cs b/DesktopClock/SettingsWindow.xaml.cs
index 6b33d81..d9b8d08 100644
--- a/DesktopClock/SettingsWindow.xaml.cs
+++ b/DesktopClock/SettingsWindow.xaml.cs
@@ -11,7 +11,6 @@
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Navigation;
-using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DesktopClock.Properties;
@@ -149,11 +148,10 @@ private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e
private void OpenSettingsFile(object sender, RoutedEventArgs e)
{
- // Save first if we can so it's up-to-date.
- if (Settings.CanBeSaved)
- Settings.Default.Save();
+ // Save first so it's up-to-date. The button is only enabled when settings can be saved.
+ Settings.Default.Save();
- // If it doesn't even exist then it's probably somewhere that requires special access and we shouldn't even be at this point.
+ // A save can still fail later, like on an unplugged drive, which leaves no file to open.
if (!Settings.Exists)
{
MessageBox.Show(this,
@@ -180,11 +178,6 @@ private void OpenSettingsFolder(object sender, RoutedEventArgs e)
{
var folderPath = Path.GetDirectoryName(Settings.FilePath);
- if (string.IsNullOrWhiteSpace(folderPath))
- {
- return;
- }
-
// Open the folder through the shell rather than passing it to explorer.exe, which splits its arguments on commas. The trailing separator makes it open as a folder, because the shell otherwise runs a program or shortcut next to it with the same name, like DesktopClock.exe next to an unzipped DesktopClock folder.
OpenUrl(folderPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
}
diff --git a/DesktopClock/Utilities/BackgroundBrushConverter.cs b/DesktopClock/Utilities/BackgroundBrushConverter.cs
index fe1f560..e1fdc1d 100644
--- a/DesktopClock/Utilities/BackgroundBrushConverter.cs
+++ b/DesktopClock/Utilities/BackgroundBrushConverter.cs
@@ -17,16 +17,16 @@ public class BackgroundBrushConverter : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
- var enabled = values.Length > 0 && values[0] is bool b && b;
+ var enabled = values[0] is bool b && b;
// Background off means outlined text on a transparent window.
if (!enabled)
return Brushes.Transparent;
- var path = values.Length > 1 ? values[1] as string : null;
- var outerColor = values.Length > 2 && values[2] is Color color ? color : Colors.Transparent;
- var opacity = values.Length > 3 && values[3] is double o ? o : 1d;
- var stretch = values.Length > 4 && values[4] is Stretch s ? s : Stretch.Fill;
+ var path = values[1] as string;
+ var outerColor = values[2] is Color color ? color : Colors.Transparent;
+ var opacity = values[3] is double o ? o : 1d;
+ var stretch = values[4] is Stretch s ? s : Stretch.Fill;
// Use the image when it loads; otherwise fall back to the solid color so the clock never ends up with an invisible background.
var image = TryLoadImage(path);
diff --git a/DesktopClock/Utilities/DesktopPinner.cs b/DesktopClock/Utilities/DesktopPinner.cs
index 36b2470..75f5f09 100644
--- a/DesktopClock/Utilities/DesktopPinner.cs
+++ b/DesktopClock/Utilities/DesktopPinner.cs
@@ -3,6 +3,7 @@
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
+using static DesktopClock.NativeMethods;
namespace DesktopClock;
@@ -166,21 +167,9 @@ private struct WindowPos
public uint Flags;
}
- [DllImport("user32.dll")]
- private static extern bool SetWindowPos(IntPtr hwnd, IntPtr insertAfter, int x, int y, int cx, int cy, uint flags);
-
- [DllImport("user32.dll")]
- private static extern IntPtr GetWindow(IntPtr hwnd, uint command);
-
[DllImport("user32.dll")]
private static extern IntPtr GetShellWindow();
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr childAfter, string className, string windowName);
-
[DllImport("user32.dll")]
private static extern IntPtr GetAncestor(IntPtr hwnd, uint flags);
-
- [DllImport("user32.dll")]
- private static extern int GetWindowLong(IntPtr hwnd, int index);
}
diff --git a/DesktopClock/Utilities/FullscreenHideManager.cs b/DesktopClock/Utilities/FullscreenHideManager.cs
index ffa8950..d4f893b 100644
--- a/DesktopClock/Utilities/FullscreenHideManager.cs
+++ b/DesktopClock/Utilities/FullscreenHideManager.cs
@@ -4,6 +4,7 @@
using System.Windows;
using System.Windows.Interop;
using DesktopClock.Properties;
+using static DesktopClock.NativeMethods;
namespace DesktopClock;
@@ -18,7 +19,7 @@ public sealed class FullscreenHideManager
public FullscreenHideManager(Window window)
{
- _window = window ?? throw new ArgumentNullException(nameof(window));
+ _window = window;
}
public void Update()
@@ -148,39 +149,6 @@ private static string GetWindowClassName(IntPtr hwnd)
[DllImport("user32.dll")]
private static extern bool IsZoomed(IntPtr hwnd);
- [DllImport("user32.dll")]
- private static extern int GetWindowLong(IntPtr hwnd, int index);
-
- [DllImport("user32.dll")]
- private static extern bool IsWindowVisible(IntPtr hwnd);
-
- [DllImport("user32.dll")]
- private static extern bool GetWindowRect(IntPtr hwnd, out NativeRect rect);
-
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern int GetClassName(IntPtr hwnd, StringBuilder className, int maxCount);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo info);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct MonitorInfo
- {
- public int cbSize;
- public NativeRect rcMonitor;
- public NativeRect rcWork;
- public uint dwFlags;
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct NativeRect
- {
- public int Left;
- public int Top;
- public int Right;
- public int Bottom;
- }
}
diff --git a/DesktopClock/Utilities/NativeMethods.cs b/DesktopClock/Utilities/NativeMethods.cs
new file mode 100644
index 0000000..2d896c1
--- /dev/null
+++ b/DesktopClock/Utilities/NativeMethods.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Windows;
+
+namespace DesktopClock;
+
+///
+/// Win32 declarations used by more than one class.
+///
+internal static class NativeMethods
+{
+ [DllImport("user32.dll")]
+ internal static extern int GetWindowLong(IntPtr hwnd, int index);
+
+ [DllImport("user32.dll")]
+ internal static extern bool SetWindowPos(IntPtr hwnd, IntPtr insertAfter, int x, int y, int cx, int cy, uint flags);
+
+ [DllImport("user32.dll")]
+ internal static extern IntPtr GetWindow(IntPtr hwnd, uint command);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ internal static extern IntPtr FindWindowEx(IntPtr parent, IntPtr childAfter, string className, string windowName);
+
+ [DllImport("user32.dll")]
+ internal static extern bool IsWindowVisible(IntPtr hwnd);
+
+ [DllImport("user32.dll")]
+ internal static extern bool GetWindowRect(IntPtr hwnd, out NativeRect rect);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ internal static extern int GetClassName(IntPtr hwnd, StringBuilder className, int maxCount);
+
+ [DllImport("user32.dll")]
+ internal static extern IntPtr MonitorFromRect(ref NativeRect rect, uint flags);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ internal static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct MonitorInfo
+ {
+ public int cbSize;
+ public NativeRect rcMonitor;
+ public NativeRect rcWork;
+ public uint dwFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct NativeRect
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+
+ public readonly Rect ToRect() => new(Left, Top, Right - Left, Bottom - Top);
+
+ public void Offset(Vector offset)
+ {
+ Left += (int)offset.X;
+ Top += (int)offset.Y;
+ Right += (int)offset.X;
+ Bottom += (int)offset.Y;
+ }
+ }
+}
diff --git a/DesktopClock/Utilities/PresetButton.cs b/DesktopClock/Utilities/PresetButton.cs
new file mode 100644
index 0000000..a83b006
--- /dev/null
+++ b/DesktopClock/Utilities/PresetButton.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Windows;
+using System.Windows.Automation;
+using System.Windows.Controls;
+
+namespace DesktopClock;
+
+///
+/// Builds the one-click preset buttons in the editors: a bold name over a smaller example of what it gives.
+///
+public static class PresetButton
+{
+ ///
+ /// Adds a preset button to the panel and returns its example text, so it can be updated later.
+ ///
+ public static TextBlock Add(Panel panel, string name, string example, Action onClick, string toolTip = null)
+ {
+ var title = new TextBlock
+ {
+ Text = name,
+ FontWeight = FontWeights.SemiBold,
+ };
+
+ var exampleText = new TextBlock
+ {
+ Text = example,
+ FontSize = 12,
+ TextTrimming = TextTrimming.CharacterEllipsis,
+ };
+ exampleText.SetResourceReference(TextBlock.ForegroundProperty, "TextSecondaryBrush");
+
+ var content = new StackPanel();
+ content.Children.Add(title);
+ content.Children.Add(exampleText);
+
+ var button = new Button
+ {
+ Content = content,
+ MaxWidth = 280,
+ HorizontalContentAlignment = HorizontalAlignment.Left,
+ Margin = new Thickness(0, 0, 8, 4),
+ ToolTip = toolTip,
+ };
+ AutomationProperties.SetName(button, name);
+ button.Click += (_, _) => onClick();
+
+ panel.Children.Add(button);
+ return exampleText;
+ }
+}
diff --git a/DesktopClock/Utilities/RelativeTimeFormatter.cs b/DesktopClock/Utilities/RelativeTimeFormatter.cs
index b345d24..dca2c43 100644
--- a/DesktopClock/Utilities/RelativeTimeFormatter.cs
+++ b/DesktopClock/Utilities/RelativeTimeFormatter.cs
@@ -58,7 +58,6 @@ public static string Format(DateTime target, DateTime now, CultureInfo culture =
private static string Days(int days, bool isFuture, CultureInfo culture) => days switch
{
- 0 => Loc.Get("RelativeNow", culture),
1 => Loc.Get(isFuture ? "RelativeTomorrow" : "RelativeYesterday", culture),
_ => Phrase(days, "RelativeDays", isFuture, culture),
};
@@ -88,7 +87,6 @@ private static int PluralFormIndex(int count, CultureInfo culture)
{
"ru" or "uk" => (mod10 == 1 && mod100 != 11) ? 0 : isFew ? 1 : 2,
"pl" => count == 1 ? 0 : isFew ? 1 : 2,
- "fr" or "pt" => count <= 1 ? 0 : 1,
_ => count == 1 ? 0 : 1,
};
}
diff --git a/DesktopClock/Utilities/SystemThemeService.cs b/DesktopClock/Utilities/SystemThemeService.cs
index 65e300f..7be2fd8 100644
--- a/DesktopClock/Utilities/SystemThemeService.cs
+++ b/DesktopClock/Utilities/SystemThemeService.cs
@@ -28,7 +28,7 @@ public static class SystemThemeService
/// Tries to build default text/outer colors that match the system's light or dark theme.
///
///
- /// Returns false when the system theme cannot be read (older Windows or missing keys).
+ /// Returns false when the theme isn't in the registry.
///
public static bool TryGetThemeDefaults(out Color textColor, out Color outerColor)
{
@@ -115,18 +115,6 @@ private static bool TryGetRegistryDword(string keyPath, string valueName, out in
value = intValue;
return true;
}
-
- if (key?.GetValue(valueName) is byte byteValue)
- {
- value = byteValue;
- return true;
- }
-
- if (key?.GetValue(valueName) is uint uintValue)
- {
- value = unchecked((int)uintValue);
- return true;
- }
}
catch
{
@@ -142,19 +130,11 @@ private static bool TryGetAccentColorFromDwm(out Color color)
{
color = default;
- try
- {
- var result = DwmGetColorizationColor(out var colorizationColor, out _);
- if (result != 0)
- return false;
-
- color = ColorFromArgbUint(colorizationColor);
- return true;
- }
- catch
- {
+ if (DwmGetColorizationColor(out var colorizationColor, out _) != 0)
return false;
- }
+
+ color = ColorFromArgbUint(colorizationColor);
+ return true;
}
///
diff --git a/DesktopClock/Utilities/ThemeManager.cs b/DesktopClock/Utilities/ThemeManager.cs
index dcba238..1bd977a 100644
--- a/DesktopClock/Utilities/ThemeManager.cs
+++ b/DesktopClock/Utilities/ThemeManager.cs
@@ -5,6 +5,7 @@
using System.Windows.Interop;
using System.Windows.Media;
using Microsoft.Win32;
+using static DesktopClock.NativeMethods;
namespace DesktopClock.Utilities;
@@ -15,7 +16,6 @@ public static class ThemeManager
{
private const string PaletteFolder = "Themes/";
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
- private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19;
private static readonly Uri LightPaletteUri = new(PaletteFolder + "LightPalette.xaml", UriKind.Relative);
private static readonly Uri DarkPaletteUri = new(PaletteFolder + "DarkPalette.xaml", UriKind.Relative);
@@ -50,17 +50,7 @@ public static void ApplyTitleBarTheme(Window window)
return;
var useDark = IsDarkTheme ? 1 : 0;
-
- try
- {
- // Try the documented attribute first, then the pre-20H1 value for older Windows 10 builds.
- if (DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useDark, sizeof(int)) != 0)
- DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, ref useDark, sizeof(int));
- }
- catch
- {
- // DWM isn't available; keep the default title bar.
- }
+ DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useDark, sizeof(int));
}
///
@@ -86,7 +76,7 @@ private static IntPtr EraseBackgroundWithThemeColor(IntPtr hwnd, int msg, IntPtr
return IntPtr.Zero;
// Read the color at message time so theme changes while the window is open apply.
- if (Application.Current?.TryFindResource("WindowBackgroundBrush") is not SolidColorBrush brush ||
+ if (Application.Current.TryFindResource("WindowBackgroundBrush") is not SolidColorBrush brush ||
!GetClientRect(hwnd, out var clientRect))
{
return IntPtr.Zero;
@@ -112,14 +102,12 @@ or UserPreferenceCategory.VisualStyle
return;
}
- Application.Current?.Dispatcher.BeginInvoke(new Action(Apply));
+ Application.Current.Dispatcher.BeginInvoke(new Action(Apply));
}
private static void Apply()
{
var app = Application.Current;
- if (app == null)
- return;
var highContrast = SystemParameters.HighContrast;
IsDarkTheme = !highContrast && !SystemThemeService.IsLightTheme();
@@ -175,20 +163,14 @@ private static Color Lighten(Color color, double amount)
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
[DllImport("user32.dll")]
- private static extern bool GetClientRect(IntPtr hwnd, out RECT rect);
+ private static extern bool GetClientRect(IntPtr hwnd, out NativeRect rect);
[DllImport("user32.dll")]
- private static extern int FillRect(IntPtr hdc, ref RECT rect, IntPtr brush);
+ private static extern int FillRect(IntPtr hdc, ref NativeRect rect, IntPtr brush);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateSolidBrush(uint colorRef);
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr obj);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct RECT
- {
- public int Left, Top, Right, Bottom;
- }
}
diff --git a/DesktopClock/Utilities/TimeStringFormatter.cs b/DesktopClock/Utilities/TimeStringFormatter.cs
index 5897ba1..17ed941 100644
--- a/DesktopClock/Utilities/TimeStringFormatter.cs
+++ b/DesktopClock/Utilities/TimeStringFormatter.cs
@@ -1,5 +1,4 @@
using System;
-using System.Globalization;
namespace DesktopClock;
@@ -7,30 +6,19 @@ public static class TimeStringFormatter
{
public static string Format(
DateTimeOffset now,
- DateTime nowDateTime,
TimeZoneInfo timeZone,
DateTime countdownTo,
string format,
string countdownFormat,
IFormatProvider formatProvider)
{
- var timeInSelectedZone = TimeZoneInfo.ConvertTime(now, timeZone);
-
- string result;
if (countdownTo == default)
- {
- result = Tokenizer.FormatWithTokenizerOrFallBack(timeInSelectedZone, format, formatProvider);
- }
- else if (string.IsNullOrWhiteSpace(countdownFormat))
- {
- result = RelativeTimeFormatter.Format(countdownTo, nowDateTime);
- }
- else
- {
- var countdown = DateTimeUtil.TimeUntil(countdownTo, now, TimeZoneInfo.Local);
- result = Tokenizer.FormatWithTokenizerOrFallBack(countdown, countdownFormat, formatProvider);
- }
+ return Tokenizer.FormatWithTokenizerOrFallBack(TimeZoneInfo.ConvertTime(now, timeZone), format, formatProvider);
+
+ if (string.IsNullOrWhiteSpace(countdownFormat))
+ return RelativeTimeFormatter.Format(countdownTo, now.DateTime);
- return result;
+ var countdown = DateTimeUtil.TimeUntil(countdownTo, now, TimeZoneInfo.Local);
+ return Tokenizer.FormatWithTokenizerOrFallBack(countdown, countdownFormat, formatProvider);
}
}
diff --git a/DesktopClock/Utilities/WindowSnapper.cs b/DesktopClock/Utilities/WindowSnapper.cs
index 08ddfdf..a41920a 100644
--- a/DesktopClock/Utilities/WindowSnapper.cs
+++ b/DesktopClock/Utilities/WindowSnapper.cs
@@ -6,6 +6,7 @@
using System.Windows.Interop;
using System.Windows.Media;
using DesktopClock.Properties;
+using static DesktopClock.NativeMethods;
namespace DesktopClock;
@@ -148,55 +149,9 @@ private static bool IsShowingClock(IntPtr hwnd)
return DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, out var cloaked, sizeof(int)) != 0 || cloaked == 0;
}
- [DllImport("user32.dll")]
- private static extern IntPtr MonitorFromRect(ref NativeRect rect, uint flags);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr childAfter, string className, string windowName);
-
- [DllImport("user32.dll")]
- private static extern bool IsWindowVisible(IntPtr hwnd);
-
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hwnd);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern int GetClassName(IntPtr hwnd, StringBuilder className, int maxCount);
-
- [DllImport("user32.dll")]
- private static extern bool GetWindowRect(IntPtr hwnd, out NativeRect rect);
-
[DllImport("dwmapi.dll")]
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out int value, int size);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct MonitorInfo
- {
- public int cbSize;
- public NativeRect rcMonitor;
- public NativeRect rcWork;
- public uint dwFlags;
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct NativeRect
- {
- public int Left;
- public int Top;
- public int Right;
- public int Bottom;
-
- public readonly Rect ToRect() => new(Left, Top, Right - Left, Bottom - Top);
-
- public void Offset(Vector offset)
- {
- Left += (int)offset.X;
- Top += (int)offset.Y;
- Right += (int)offset.X;
- Bottom += (int)offset.Y;
- }
- }
}
diff --git a/DesktopClock/Utilities/WindowUtil.cs b/DesktopClock/Utilities/WindowUtil.cs
index 7c784e4..1beae89 100644
--- a/DesktopClock/Utilities/WindowUtil.cs
+++ b/DesktopClock/Utilities/WindowUtil.cs
@@ -5,6 +5,7 @@
using System.Windows.Input;
using System.Windows.Interop;
using WpfWindowPlacement;
+using static DesktopClock.NativeMethods;
namespace DesktopClock;
@@ -49,22 +50,9 @@ public static Vector GetKeyboardNudge(Key key, ModifierKeys modifiers)
private static readonly IntPtr HWND_TOPMOST = new(-1);
- [DllImport("user32.dll")]
- private static extern int GetWindowLong(IntPtr hwnd, int index);
-
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
- [DllImport("user32.dll")]
- private static extern bool SetWindowPos(
- IntPtr hwnd,
- IntPtr insertAfter,
- int x,
- int y,
- int cx,
- int cy,
- uint flags);
-
private const int ASFW_ANY = -1;
[DllImport("user32.dll")]
@@ -212,44 +200,8 @@ static NativeRect MoveIntoNearestWorkArea(NativeRect rect)
private const uint MONITOR_DEFAULTTONULL = 0;
private const uint MONITOR_DEFAULTTONEAREST = 2;
- [DllImport("user32.dll")]
- private static extern IntPtr MonitorFromRect(ref NativeRect rect, uint flags);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct MonitorInfo
- {
- public int cbSize;
- public NativeRect rcMonitor;
- public NativeRect rcWork;
- public uint dwFlags;
- }
-
private const uint GW_HWNDPREV = 3;
- [DllImport("user32.dll")]
- private static extern IntPtr GetWindow(IntPtr hwnd, uint command);
-
- [DllImport("user32.dll")]
- private static extern bool IsWindowVisible(IntPtr hwnd);
-
- [DllImport("user32.dll")]
- private static extern bool GetWindowRect(IntPtr hwnd, out NativeRect rect);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern int GetClassName(IntPtr hwnd, StringBuilder className, int maxCount);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct NativeRect
- {
- public int Left;
- public int Top;
- public int Right;
- public int Bottom;
- }
-
///
/// Returns the extended style that matches the requested taskbar and Alt+Tab behavior.
///