MainWindow.xaml.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. using System;
  2. using System.ComponentModel;
  3. using System.Diagnostics;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Input;
  9. using CommunityToolkit.Mvvm.ComponentModel;
  10. using CommunityToolkit.Mvvm.Input;
  11. using DesktopClock.Properties;
  12. using H.NotifyIcon;
  13. using Humanizer;
  14. namespace DesktopClock;
  15. /// <summary>
  16. /// Interaction logic for MainWindow.xaml
  17. /// </summary>
  18. [ObservableObject]
  19. public partial class MainWindow : Window
  20. {
  21. private bool _hasInitiallyChangedSize;
  22. private readonly SystemClockTimer _systemClockTimer;
  23. private TaskbarIcon _trayIcon;
  24. private TimeZoneInfo _timeZone;
  25. /// <summary>
  26. /// The date and time to countdown to, or null if regular clock is desired.
  27. /// </summary>
  28. [ObservableProperty]
  29. private DateTimeOffset? _countdownTo;
  30. /// <summary>
  31. /// The current date and time in the selected time zone or countdown as a formatted string.
  32. /// </summary>
  33. [ObservableProperty]
  34. private string _currentTimeOrCountdownString;
  35. public MainWindow()
  36. {
  37. InitializeComponent();
  38. DataContext = this;
  39. _timeZone = App.GetTimeZone();
  40. UpdateCountdownEnabled();
  41. Settings.Default.PropertyChanged += (s, e) => Dispatcher.Invoke(() => Settings_PropertyChanged(s, e));
  42. _systemClockTimer = new();
  43. _systemClockTimer.SecondChanged += SystemClockTimer_SecondChanged;
  44. _systemClockTimer.Start();
  45. ContextMenu = Resources["MainContextMenu"] as ContextMenu;
  46. ConfigureTrayIcon(!Settings.Default.ShowInTaskbar, true);
  47. }
  48. [RelayCommand]
  49. public void CopyToClipboard() => Clipboard.SetText(CurrentTimeOrCountdownString);
  50. /// <summary>
  51. /// Sets app's theme to given value.
  52. /// </summary>
  53. [RelayCommand]
  54. public void SetTheme(Theme theme) => Settings.Default.Theme = theme;
  55. /// <summary>
  56. /// Sets format string in settings to given string.
  57. /// </summary>
  58. [RelayCommand]
  59. public void SetFormat(string format) => Settings.Default.Format = format;
  60. /// <summary>
  61. /// Sets time zone ID in settings to given time zone ID.
  62. /// </summary>
  63. [RelayCommand]
  64. public void SetTimeZone(TimeZoneInfo tzi) => App.SetTimeZone(tzi);
  65. /// <summary>
  66. /// Creates a new clock executable and starts it.
  67. /// </summary>
  68. [RelayCommand]
  69. public void NewClock()
  70. {
  71. var result = MessageBox.Show(this,
  72. $"This will copy the executable and start it with new settings.\n\n" +
  73. $"Continue?",
  74. Title, MessageBoxButton.OKCancel, MessageBoxImage.Question, MessageBoxResult.OK);
  75. if (result != MessageBoxResult.OK)
  76. return;
  77. var newExePath = Path.Combine(App.MainFileInfo.DirectoryName, App.MainFileInfo.GetFileAtNextIndex().Name);
  78. // Copy and start the new clock.
  79. File.Copy(App.MainFileInfo.FullName, newExePath);
  80. Process.Start(newExePath);
  81. }
  82. /// <summary>
  83. /// Explains how to enable countdown mode, then asks user if they want to view Advanced settings to do so.
  84. /// </summary>
  85. [RelayCommand]
  86. public void CountdownWizard()
  87. {
  88. var result = MessageBox.Show(this,
  89. $"In advanced settings: change \"{nameof(Settings.Default.CountdownTo)}\" in the format of \"{default(DateTime)}\", then save." +
  90. "\n\nOpen advanced settings now?",
  91. Title, MessageBoxButton.OKCancel, MessageBoxImage.Question, MessageBoxResult.OK);
  92. if (result != MessageBoxResult.OK)
  93. return;
  94. OpenSettings();
  95. }
  96. /// <summary>
  97. /// Opens the settings file in Notepad.
  98. /// </summary>
  99. [RelayCommand]
  100. public void OpenSettings()
  101. {
  102. // Save first so it's up-to-date.
  103. Settings.Default.Save();
  104. // Open settings file in notepad.
  105. try
  106. {
  107. Process.Start("notepad", Settings.FilePath);
  108. }
  109. catch (Exception ex)
  110. {
  111. // Lazy scammers on the Microsoft Store may reupload without realizing it gets sandboxed, making it unable to start the Notepad process (#1, #12).
  112. MessageBox.Show(this,
  113. "Couldn't open settings file.\n\n" +
  114. "This app may have be reuploaded without permission. If you paid for it, ask for a refund and download it for free from the original source: https://github.com/danielchalmers/DesktopClock.\n\n" +
  115. $"If it still doesn't work, create a new Issue at that link with details on what happened and include this error: \"{ex.Message}\"",
  116. Title, MessageBoxButton.OK, MessageBoxImage.Error);
  117. }
  118. }
  119. /// <summary>
  120. /// Opens the GitHub Releases page.
  121. /// </summary>
  122. [RelayCommand]
  123. public void CheckForUpdates()
  124. {
  125. Process.Start("https://github.com/danielchalmers/DesktopClock/releases");
  126. }
  127. /// <summary>
  128. /// Exits the program.
  129. /// </summary>
  130. [RelayCommand]
  131. public void Exit()
  132. {
  133. Close();
  134. }
  135. private void ConfigureTrayIcon(bool showIcon, bool firstLaunch)
  136. {
  137. if (showIcon)
  138. {
  139. if (_trayIcon == null)
  140. {
  141. _trayIcon = Resources["TrayIcon"] as TaskbarIcon;
  142. _trayIcon.ContextMenu = Resources["MainContextMenu"] as ContextMenu;
  143. _trayIcon.ContextMenu.DataContext = this;
  144. _trayIcon.ForceCreate(enablesEfficiencyMode: false);
  145. _trayIcon.TrayLeftMouseDoubleClick += (_, _) => Activate();
  146. }
  147. if (!firstLaunch)
  148. _trayIcon.ShowNotification("Hidden from taskbar", "Icon was moved to the tray");
  149. }
  150. else
  151. {
  152. _trayIcon?.Dispose();
  153. _trayIcon = null;
  154. }
  155. }
  156. private void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e)
  157. {
  158. switch (e.PropertyName)
  159. {
  160. case nameof(Settings.Default.TimeZone):
  161. _timeZone = App.GetTimeZone();
  162. UpdateTimeString();
  163. break;
  164. case nameof(Settings.Default.Format):
  165. UpdateTimeString();
  166. break;
  167. case nameof(Settings.Default.ShowInTaskbar):
  168. ConfigureTrayIcon(!Settings.Default.ShowInTaskbar, false);
  169. break;
  170. case nameof(Settings.Default.CountdownTo):
  171. UpdateCountdownEnabled();
  172. break;
  173. }
  174. }
  175. private void SystemClockTimer_SecondChanged(object sender, EventArgs e)
  176. {
  177. UpdateTimeString();
  178. }
  179. private void UpdateCountdownEnabled()
  180. {
  181. if (Settings.Default.CountdownTo == null || Settings.Default.CountdownTo == default(DateTime))
  182. {
  183. CountdownTo = null;
  184. return;
  185. }
  186. CountdownTo = new DateTimeOffset(Settings.Default.CountdownTo.Value, _timeZone.BaseUtcOffset);
  187. }
  188. private void UpdateTimeString()
  189. {
  190. string GetTimeString()
  191. {
  192. var timeInSelectedZone = TimeZoneInfo.ConvertTime(DateTimeOffset.Now, _timeZone);
  193. if (CountdownTo == null)
  194. {
  195. return Tokenizer.FormatWithTokenizerOrFallBack(timeInSelectedZone, Settings.Default.Format, CultureInfo.DefaultThreadCurrentCulture);
  196. }
  197. else
  198. {
  199. if (string.IsNullOrWhiteSpace(Settings.Default.CountdownFormat))
  200. return CountdownTo.Humanize(timeInSelectedZone);
  201. return Tokenizer.FormatWithTokenizerOrFallBack(Settings.Default.CountdownTo - timeInSelectedZone, Settings.Default.CountdownFormat, CultureInfo.DefaultThreadCurrentCulture);
  202. }
  203. }
  204. CurrentTimeOrCountdownString = GetTimeString();
  205. }
  206. private void Window_MouseDown(object sender, MouseButtonEventArgs e)
  207. {
  208. if (e.ChangedButton == MouseButton.Left && Settings.Default.DragToMove)
  209. {
  210. DragMove();
  211. }
  212. }
  213. private void Window_MouseDoubleClick(object sender, MouseButtonEventArgs e)
  214. {
  215. CopyToClipboard();
  216. }
  217. private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
  218. {
  219. if (Keyboard.Modifiers == ModifierKeys.Control)
  220. {
  221. // Scale size based on scroll amount, with one notch on a default PC mouse being a change of 15%.
  222. var steps = e.Delta / (double)Mouse.MouseWheelDeltaForOneLine;
  223. var change = Settings.Default.Height * steps * 0.15;
  224. Settings.Default.Height = (int)Math.Min(Math.Max(Settings.Default.Height + change, 16), 160);
  225. }
  226. }
  227. private void Window_ContentRendered(object sender, EventArgs e)
  228. {
  229. SizeChanged += Window_SizeChanged;
  230. if (!Settings.CanBeSaved)
  231. {
  232. MessageBox.Show(this,
  233. $"Settings won't be saved because of an access error. Make sure {Title} is in a folder that can be written to without administrator privileges!",
  234. Title, MessageBoxButton.OK, MessageBoxImage.Warning);
  235. }
  236. }
  237. private void Window_Closed(object sender, EventArgs e)
  238. {
  239. // Stop the file watcher before saving.
  240. Settings.Default.Dispose();
  241. if (Settings.CanBeSaved)
  242. Settings.Default.Save();
  243. App.SetRunOnStartup(Settings.Default.RunOnStartup);
  244. }
  245. private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
  246. {
  247. if (_hasInitiallyChangedSize && e.WidthChanged && Settings.Default.RightAligned)
  248. {
  249. var previousRight = Left + e.PreviousSize.Width;
  250. Left = previousRight - ActualWidth;
  251. }
  252. // Use this to ignore the change when the window is loaded at the beginning.
  253. _hasInitiallyChangedSize = true;
  254. }
  255. }