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. ///