Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions DesktopClock.Tests/BackgroundBrushConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace DesktopClock.Tests;

public class BackgroundBrushConverterTests
{
[Theory]
[InlineData("{0}")]
[InlineData("\"{0}\"")] // Copy as path in Explorer adds quotes.
[InlineData(" \"{0}\" ")]
public void Convert_ShowsTheImage(string pathFormat)
{
var image = Path.Combine(Path.GetTempPath(), $"DesktopClock background {Guid.NewGuid():N}.png");
try
{
using (var file = File.Create(image))
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(BitmapSource.Create(2, 2, 96, 96, PixelFormats.Bgra32, null, new byte[16], 8)));
encoder.Save(file);
}

var brush = Convert(string.Format(pathFormat, image));

Assert.IsType<ImageBrush>(brush);
}
finally
{
File.Delete(image);
}
}

[Fact]
public void Convert_FallsBackToTheColorWhenTheImageIsMissing()
{
var brush = Convert(Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.png"));

Assert.Equal(Colors.Red, Assert.IsType<SolidColorBrush>(brush).Color);
}

private static object Convert(string path) =>
new BackgroundBrushConverter().Convert([true, path, Colors.Red, 1d, Stretch.Fill], typeof(Brush), null, CultureInfo.InvariantCulture);
}
9 changes: 9 additions & 0 deletions DesktopClock.Tests/FileUtilTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,13 @@ public void IsInTempFolder_ShouldNotMatchAFolderNextToIt()

Assert.False(exe.IsInTempFolder());
}

[Theory]
[InlineData("\"C:\\My Sounds\\chime.wav\"", "C:\\My Sounds\\chime.wav")]
[InlineData("C:\\My Sounds\\chime.wav ", "C:\\My Sounds\\chime.wav")]
[InlineData("", "")]
public void UnquotePath_ShouldRemoveTheQuotesFromCopyAsPath(string path, string expected)
{
Assert.Equal(expected, FileUtil.UnquotePath(path));
}
}
15 changes: 15 additions & 0 deletions DesktopClock.Tests/FormatPresetsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ public void DefaultClockFormat_ReadsNaturallyInTheRegion(string culture, string
Assert.Equal(expected, Tokenizer.FormatWithTokenizerOrFallBack(Sample, format, Region(culture)));
}

// Windows lets people switch only the short time, which the taskbar shows, between 12 and 24 hours.
[Theory]
[InlineData("en-US", "HH:mm", "{HH:mm:ss}")]
[InlineData("en-US", "H:mm", "{H:mm:ss}")]
[InlineData("en-GB", "h:mm tt", "{h:mm:ss tt}")]
[InlineData("en-US", "hh:mm tt", "{h:mm:ss tt}")] // Still 12-hour, so the regional time with seconds stays.
public void TimeWithSeconds_FollowsAShortTimeSwitchedBetween12And24Hours(string culture, string shortTime, string expected)
{
var region = (CultureInfo)Region(culture).Clone();
region.DateTimeFormat.ShortTimePattern = shortTime;

Assert.Equal(expected, FormatPresets.ForClock(region).Single(p => p.Name == "Time with seconds").Format);
Assert.EndsWith(expected, FormatPresets.DefaultClockFormat(region));
}

[Theory]
[InlineData("en-US", "Tue, Sep 22, 1:01 PM")]
[InlineData("fr-FR", "mar. 22 sept. 13:01")]
Expand Down
50 changes: 50 additions & 0 deletions DesktopClock.Tests/WheelNotchesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace DesktopClock.Tests;

public class WheelNotchesTests
{
[Theory]
[InlineData(120, 1)]
[InlineData(-120, -1)]
[InlineData(360, 3)]
public void Add_CountsWholeNotchesFromAWheel(int delta, int expected)
{
Assert.Equal(expected, new WheelNotches().Add(delta));
}

[Fact]
public void Add_KeepsTheRestOfANotchForNextTime()
{
var wheel = new WheelNotches();

Assert.Equal(0, wheel.Add(100));
Assert.Equal(1, wheel.Add(40));
Assert.Equal(1, wheel.Add(100));
}

[Fact]
public void Add_ScrollingBackCancelsOut()
{
var wheel = new WheelNotches();

Assert.Equal(0, wheel.Add(60));
Assert.Equal(0, wheel.Add(-60));
Assert.Equal(0, wheel.Add(60));
}

[Fact]
public void TouchpadScroll_ResizesLikeOneNotch()
{
// A precision touchpad sends many small deltas, here twelve of a twelfth of a notch.
var wheel = new WheelNotches();
var height = 48;
for (var i = 0; i < 12; i++)
{
var notches = wheel.Add(10);
if (notches != 0)
height = HeightScaleConverter.ScaleHeight(height, notches);
}

Assert.Equal(HeightScaleConverter.ScaleHeight(48, 1), height);
Assert.NotEqual(48, height);
}
}
15 changes: 11 additions & 4 deletions DesktopClock/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Media;
using System.Runtime.InteropServices;
using System.Text;
Expand Down Expand Up @@ -35,6 +36,7 @@ public partial class MainWindow : Window
private bool _toldHowToShow;
private FullscreenHideManager _fullscreenHideManager;
private DesktopPinner _desktopPinner;
private readonly WheelNotches _wheelNotches = new();
private readonly PropertyChangedEventHandler _settingsPropertyChanged;

/// <summary>
Expand Down Expand Up @@ -390,7 +392,7 @@ private void TryPlaySound(DateTimeOffset second)
var previous = _previousSecond;
_previousSecond = second;

var path = Settings.Default.WavFilePath;
var path = FileUtil.UnquotePath(Settings.Default.WavFilePath);
if (string.IsNullOrWhiteSpace(path) || (Settings.Default.WavFileInterval == default && !Settings.Default.PlaySoundOnCountdown))
return;

Expand Down Expand Up @@ -494,9 +496,10 @@ private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
// Resize the window when scrolling if the Ctrl key is pressed.
if (Keyboard.Modifiers == ModifierKeys.Control)
{
// Amount of scroll that occurred and whether it was positive or negative.
var steps = e.Delta / (double)Mouse.MouseWheelDeltaForOneLine;
Settings.Default.Height = HeightScaleConverter.ScaleHeight(Settings.Default.Height, steps);
// A fraction of a notch on its own rounds back to the same height, so touchpads only resize once they add up to a notch.
var notches = _wheelNotches.Add(e.Delta);
if (notches != 0)
Settings.Default.Height = HeightScaleConverter.ScaleHeight(Settings.Default.Height, notches);
}
}

Expand Down Expand Up @@ -551,6 +554,10 @@ private void Window_ContentRendered(object sender, EventArgs e)

private void Window_Closing(object sender, CancelEventArgs e)
{
// Close Settings first, which commits a value still being typed, like a countdown target, so it's in the save below. Exiting closes it anyway, but only after this save.
foreach (var settingsWindow in Application.Current.Windows.OfType<SettingsWindow>().ToList())
settingsWindow.Close();

// Save the last text and the placement to preserve dimensions and position of the clock.
PixelShifter?.RestoreBasePosition(this);
Settings.Default.LastDisplay = CurrentTimeOrCountdownString;
Expand Down
2 changes: 1 addition & 1 deletion DesktopClock/Utilities/BackgroundBrushConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private static ImageSource TryLoadImage(string path)
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad; // Decode now so a missing or invalid file fails here, and don't hold the file locked.
bitmap.UriSource = new Uri(path, UriKind.Absolute);
bitmap.UriSource = new Uri(FileUtil.UnquotePath(path), UriKind.Absolute);
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
Expand Down
5 changes: 5 additions & 0 deletions DesktopClock/Utilities/FileUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public static FileInfo GetFileAtNextIndex(this FileInfo fileInfo)
return file;
}

/// <summary>
/// Removes the quotes that Copy as path in Explorer puts around a path, so a pasted path works like a typed one.
/// </summary>
public static string UnquotePath(string path) => path?.Trim().Trim('"');

/// <summary>
/// Whether the file is in the temp folder, which is where Windows puts an app opened straight from inside a zip, in a new folder each time.
/// </summary>
Expand Down
23 changes: 19 additions & 4 deletions DesktopClock/Utilities/FormatPresets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static class FormatPresets
public static string DefaultClockFormat(CultureInfo culture)
{
var format = culture.DateTimeFormat;
return WithTime(culture, WeekdayAndDate(culture, "{ddd}", Token(ShortMonthDayPattern(format))), Token(format.LongTimePattern));
return WithTime(culture, WeekdayAndDate(culture, "{ddd}", Token(ShortMonthDayPattern(format))), Token(TimeWithSeconds(format)));
}

/// <summary>
Expand All @@ -34,7 +34,7 @@ public static string DefaultClockFormat(CultureInfo culture)
if (Uses12HourClock(format))
presets.Add((Loc.Get("ClockPresetTime24"), "{HH:mm}"));

presets.Add((Loc.Get("ClockPresetTimeSeconds"), Token(format.LongTimePattern)));
presets.Add((Loc.Get("ClockPresetTimeSeconds"), Token(TimeWithSeconds(format))));
presets.Add((Loc.Get("ClockPresetDayTime"), WithTime(culture, "{ddd}", time)));
var dateTime = WithTime(culture, WeekdayAndDate(culture, "{ddd}", Token(ShortMonthDayPattern(format))), time);
var fullDateTime = WithTime(culture, WeekdayAndDate(culture, "{dddd}", monthDay), time);
Expand Down Expand Up @@ -120,9 +120,24 @@ public static string ShortMonthDayPattern(DateTimeFormatInfo format)
}

/// <summary>
/// Whether the regional time format uses a 12-hour clock. Quoted text is skipped so a literal like the "h" in Canadian French "HH 'h' mm" doesn't count.
/// Whether the regional time format uses a 12-hour clock.
/// </summary>
public static bool Uses12HourClock(DateTimeFormatInfo format) => Regex.Replace(format.ShortTimePattern, "'[^']*'", "").Contains("h");
public static bool Uses12HourClock(DateTimeFormatInfo format) => Uses12Hours(format.ShortTimePattern);

/// <summary>
/// The regional time with seconds, such as "h:mm:ss tt" in the US. Where someone switched only the short time between 12 and 24 hours, which is what the taskbar shows, it follows the short time instead.
/// </summary>
private static string TimeWithSeconds(DateTimeFormatInfo format)
{
if (Uses12Hours(format.ShortTimePattern) == Uses12Hours(format.LongTimePattern))
return format.LongTimePattern;

var minutes = format.ShortTimePattern.IndexOf("mm", StringComparison.Ordinal);
return minutes < 0 ? format.ShortTimePattern : format.ShortTimePattern.Insert(minutes + 2, ":ss");
}

// Quoted text is skipped so a literal like the "h" in Canadian French "HH 'h' mm" doesn't count.
private static bool Uses12Hours(string pattern) => Regex.Replace(pattern, "'[^']*'", "").Contains("h");

/// <summary>
/// Puts the weekday and date in the order and with the punctuation the region uses: "Tue, Sep 22", "mar. 22 sept." in French, "22 Eyl Sal" in Turkish, and date first in Chinese, Japanese, and Korean, with a short weekday in parentheses where they use them ("9月22日(火)", "9월 22일 (화)").
Expand Down
22 changes: 22 additions & 0 deletions DesktopClock/Utilities/WheelNotches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Windows.Input;

namespace DesktopClock;

/// <summary>
/// Adds up mouse wheel movement into whole notches, so touchpads and smooth-scrolling mice, which send a fraction of a notch at a time, count like a wheel.
/// </summary>
public sealed class WheelNotches
{
private int _delta;

/// <summary>
/// Adds a wheel delta and returns the whole notches it completes, keeping the rest for next time.
/// </summary>
public int Add(int delta)
{
_delta += delta;
var notches = _delta / Mouse.MouseWheelDeltaForOneLine;
_delta %= Mouse.MouseWheelDeltaForOneLine;
return notches;
}
}
Loading