├── Hourglass.Bundle ├── .gitignore ├── Logo.png ├── Bundle.wxs └── MIT.rtf ├── Hourglass.Setup ├── .gitignore └── Product.wxs ├── Update-Version.bat ├── Hourglass ├── Lib │ ├── TaskDialog │ │ ├── github.url │ │ └── TaskDialog │ │ │ ├── TaskDialogStartupLocation.cs │ │ │ ├── TaskDialogStandardIconContainer.cs │ │ │ ├── TaskDialogHyperlinkClickedEventArgs.cs │ │ │ ├── TaskDialogCustomButtonStyle.cs │ │ │ ├── TaskDialogClosingEventArgs.cs │ │ │ ├── TaskDialogButtonClickedEventArgs.cs │ │ │ ├── TaskDialogIconHandle.cs │ │ │ ├── TaskDialogProgressBarState.cs │ │ │ ├── TaskDialogStandardIcon.cs │ │ │ ├── TaskDialogButtons.cs │ │ │ ├── TaskDialogResult.cs │ │ │ ├── TaskDialog.WindowSubclassHandler.cs │ │ │ ├── WindowSubclassHandlerNativeMethods.cs │ │ │ ├── TaskDialogIcon.cs │ │ │ ├── TaskDialogCheckbox.cs │ │ │ ├── TaskDialogCustomButton.cs │ │ │ ├── TaskDialogRadioButtonCollection.cs │ │ │ ├── TaskDialogCustomButtonCollection.cs │ │ │ ├── TaskDialogControl.cs │ │ │ ├── TaskDialogFooter.cs │ │ │ └── TaskDialogExpander.cs │ └── WindowsVirtualDesktopHelper │ │ ├── github.url │ │ └── Source │ │ └── VirtualDesktopAPI │ │ ├── Implementation │ │ ├── VirtualDesktopWin10.cs │ │ ├── VirtualDesktopWin11_21H2.cs │ │ ├── VirtualDesktopWin11_22H2.cs │ │ ├── VirtualDesktopWin11_Insider.cs │ │ ├── VirtualDesktopWin11_23H2.cs │ │ ├── VirtualDesktopWin11_23H2_2921.cs │ │ ├── VirtualDesktopWin11_Insider22631.cs │ │ └── VirtualDesktopWin11_Insider25314.cs │ │ ├── ImmersiveShellProvider.cs │ │ └── VirtualDesktop.cs ├── Resources │ ├── AppIcon.ico │ ├── BeepLoud.wav │ ├── BeepQuiet.wav │ ├── PauseIcon.ico │ ├── StartIcon.ico │ ├── StopIcon.ico │ ├── TrayIcon.ico │ ├── BeepNormal.wav │ ├── RestartIcon.ico │ ├── ResumeIcon.ico │ ├── StopIconGrayed.ico │ ├── PauseIconGrayed.ico │ ├── RestartIconGrayed.ico │ ├── ResumeIconGrayed.ico │ ├── StartIconGrayed.ico │ ├── MergeIcons.bat │ ├── MergeIcons.txt │ └── License.txt ├── Windows │ ├── Audio │ │ ├── IAudioPlayer.cs │ │ ├── AudioPlayer.cs │ │ ├── AudioPlayerSelector.cs │ │ └── NAudioPlayer.cs │ ├── VisibilityToBooleanConverter.cs │ ├── RegexMatchConverter.cs │ ├── ColorControl.xaml │ ├── ErrorDialog.xaml.cs │ ├── ColorControl.xaml.cs │ ├── ErrorDialog.xaml │ ├── UsageDialog.xaml │ ├── UsageDialog.xaml.cs │ └── AboutDialog.xaml.cs ├── Managers │ ├── TaskDialogManager.cs │ ├── SettingsManager.cs │ ├── NotificationAreaIconManager.cs │ ├── Manager.cs │ ├── VirtualDesktopManager.cs │ ├── AppManager.cs │ ├── TimerOptionsManager.cs │ ├── TimerStartManager.cs │ └── KeepAwakeManager.cs ├── Extensions │ ├── BoolExtensions.cs │ ├── UriExtensions.cs │ ├── AssemblyExtensions.cs │ ├── WpfExtensions.cs │ ├── DisposableExtensions.cs │ ├── HotkeyCharExtensions.cs │ ├── ExceptionExtensions.cs │ ├── ApplicationExtensions.cs │ ├── NotifyPropertyChangedExtensions.cs │ ├── IRestorableWindow.cs │ ├── WindowsExtensions.cs │ ├── FrameworkElementExtensions.cs │ ├── EnvironmentExtensions.cs │ ├── ColorExtensions.cs │ ├── CultureInfoExtensions.cs │ ├── DependencyObjectExtensions.cs │ ├── ResourceManagerExtensions.cs │ ├── MathExtensions.cs │ └── DayOfWeekExtensions.cs ├── Urls.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── App.manifest │ ├── Settings.settings │ └── Settings.cs ├── Serialization │ ├── UpdateInfo.cs │ ├── TimerStartInfo.cs │ ├── ThemeInfoList.cs │ ├── TimerInfoList.cs │ ├── TimerStartInfoList.cs │ ├── WindowSizeInfo.cs │ └── ThemeInfo.cs ├── ngen-Hourglass.bat ├── Parsing │ ├── EmptyTimeToken.cs │ └── EmptyDateToken.cs └── App.config ├── Tools └── ResourceHacker │ ├── ResourceHacker.url │ ├── ResourceHacker.exe │ └── ReadMe.txt ├── global.json ├── Directory.Build.props ├── Hourglass.Test ├── UnitTestingSettings.cs └── Hourglass.Test.csproj ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── latest.xml ├── .editorconfig ├── LICENSE.md └── Update-Version.ps1 /Hourglass.Bundle/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ 3 | -------------------------------------------------------------------------------- /Hourglass.Setup/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ 3 | -------------------------------------------------------------------------------- /Update-Version.bat: -------------------------------------------------------------------------------- 1 | pwsh.exe -NoLogo -NoProfile -NonInteractive -Command "& '%~dpn0.ps1'" %* 2 | -------------------------------------------------------------------------------- /Hourglass.Bundle/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass.Bundle/Logo.png -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/github.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://github.com/kpreisser/TaskDialog 3 | -------------------------------------------------------------------------------- /Hourglass/Resources/AppIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/AppIcon.ico -------------------------------------------------------------------------------- /Tools/ResourceHacker/ResourceHacker.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://www.angusj.com/resourcehacker/ 3 | -------------------------------------------------------------------------------- /Hourglass/Resources/BeepLoud.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/BeepLoud.wav -------------------------------------------------------------------------------- /Hourglass/Resources/BeepQuiet.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/BeepQuiet.wav -------------------------------------------------------------------------------- /Hourglass/Resources/PauseIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/PauseIcon.ico -------------------------------------------------------------------------------- /Hourglass/Resources/StartIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/StartIcon.ico -------------------------------------------------------------------------------- /Hourglass/Resources/StopIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/StopIcon.ico -------------------------------------------------------------------------------- /Hourglass/Resources/TrayIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/TrayIcon.ico -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hourglass/Resources/BeepNormal.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/BeepNormal.wav -------------------------------------------------------------------------------- /Hourglass/Resources/RestartIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/RestartIcon.ico -------------------------------------------------------------------------------- /Hourglass/Resources/ResumeIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/ResumeIcon.ico -------------------------------------------------------------------------------- /Hourglass/Resources/StopIconGrayed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/StopIconGrayed.ico -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.15.53.0 4 | 5 | 6 | -------------------------------------------------------------------------------- /Hourglass/Resources/PauseIconGrayed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/PauseIconGrayed.ico -------------------------------------------------------------------------------- /Hourglass/Resources/RestartIconGrayed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/RestartIconGrayed.ico -------------------------------------------------------------------------------- /Hourglass/Resources/ResumeIconGrayed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/ResumeIconGrayed.ico -------------------------------------------------------------------------------- /Hourglass/Resources/StartIconGrayed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Hourglass/Resources/StartIconGrayed.ico -------------------------------------------------------------------------------- /Tools/ResourceHacker/ResourceHacker.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i2van/hourglass/HEAD/Tools/ResourceHacker/ResourceHacker.exe -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/github.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://github.com/dankrusi/WindowsVirtualDesktopHelper 3 | -------------------------------------------------------------------------------- /Hourglass.Test/UnitTestingSettings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] -------------------------------------------------------------------------------- /Hourglass/Resources/MergeIcons.bat: -------------------------------------------------------------------------------- 1 | cd "%~dp0" 2 | start/wait "Merge icons" "%~dp0..\..\..\..\..\..\hourglass\Tools\ResourceHacker\ResourceHacker.exe" -script "MergeIcons.txt" -------------------------------------------------------------------------------- /Hourglass/Windows/Audio/IAudioPlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Hourglass.Windows.Audio; 4 | 5 | public interface IAudioPlayer : IDisposable 6 | { 7 | void Open(string uri); 8 | void Play(); 9 | void Stop(); 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "nuget" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /latest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.15.53.0 4 | https://github.com/i2van/hourglass/releases/latest 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.IDE0130.severity = none 3 | dotnet_diagnostic.IDE0290.severity = none 4 | dotnet_diagnostic.IDE0305.severity = none 5 | dotnet_diagnostic.S2325.severity = none 6 | dotnet_diagnostic.S3358.severity = none 7 | dotnet_diagnostic.S3881.severity = none 8 | dotnet_diagnostic.S6562.severity = none -------------------------------------------------------------------------------- /Hourglass/Resources/MergeIcons.txt: -------------------------------------------------------------------------------- 1 | [FILENAMES] 2 | Exe=..\Hourglass.exe 3 | SaveAs=..\Hourglass.exe 4 | Log=..\MergeIcons.log 5 | [COMMANDS] 6 | -add StartIcon.ico, ICONGROUP,32513, 7 | -add PauseIcon.ico, ICONGROUP,32514, 8 | -add ResumeIcon.ico, ICONGROUP,32515, 9 | -add StopIcon.ico, ICONGROUP,32516, 10 | -add RestartIcon.ico, ICONGROUP,32517, -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogStartupLocation.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable all 2 | 3 | namespace KPreisser.UI; 4 | 5 | /// 6 | /// 7 | /// 8 | public enum TaskDialogStartupLocation 9 | { 10 | /// 11 | /// 12 | /// 13 | CenterScreen = 0, 14 | 15 | /// 16 | /// 17 | /// 18 | CenterParent = 1 19 | } -------------------------------------------------------------------------------- /Hourglass/Managers/TaskDialogManager.cs: -------------------------------------------------------------------------------- 1 | using Hourglass.Extensions; 2 | 3 | namespace Hourglass.Managers; 4 | 5 | public sealed class TaskDialogManager : Manager 6 | { 7 | public static readonly TaskDialogManager Instance = new(); 8 | 9 | private TaskDialogManager() 10 | { 11 | } 12 | 13 | protected override void Dispose(bool disposing) => 14 | WindowExtensions.Clean(); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogStandardIconContainer.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable all 2 | 3 | namespace KPreisser.UI; 4 | 5 | internal class TaskDialogStandardIconContainer : TaskDialogIcon 6 | { 7 | public TaskDialogStandardIconContainer(TaskDialogStandardIcon icon) 8 | { 9 | Icon = icon; 10 | } 11 | 12 | public TaskDialogStandardIcon Icon 13 | { 14 | get; 15 | } 16 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/BoolExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace Hourglass.Extensions; 4 | 5 | internal static class BoolExtensions 6 | { 7 | extension(bool visible) 8 | { 9 | public Visibility ToVisibility() => 10 | visible ? Visibility.Visible : Visibility.Collapsed; 11 | 12 | public Visibility ToVisibilityReversed() => 13 | ToVisibility(!visible); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Hourglass/Urls.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Hourglass.Properties; 4 | 5 | namespace Hourglass; 6 | 7 | internal static class Urls 8 | { 9 | public static readonly Uri FAQ = new(Resources.FAQUrl); 10 | public static readonly Uri Usage = new(Resources.UsageUrl); 11 | public static readonly Uri Readme = new(Resources.ReadmeUrl); 12 | public static readonly Uri NewIssue = new(Resources.NewIssueUrl); 13 | } 14 | -------------------------------------------------------------------------------- /Hourglass/Extensions/UriExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | using Hourglass.Properties; 5 | 6 | // ReSharper disable ExceptionNotDocumented 7 | 8 | namespace Hourglass.Extensions; 9 | 10 | internal static class UriExtensions 11 | { 12 | public static readonly Uri FAQUri = new(Resources.FAQUrl); 13 | 14 | public static void Navigate(this Uri uri) => 15 | Process.Start(uri.ToString()); 16 | } 17 | -------------------------------------------------------------------------------- /Hourglass/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | namespace Hourglass.Extensions; 5 | 6 | internal static class AssemblyExtensions 7 | { 8 | public static string GetExecutableDirectoryName() 9 | { 10 | var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase) ?? "."; 11 | 12 | return path.StartsWith("file:") 13 | ? path.Remove(0, 6) // file:\ 14 | : path; 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Hourglass/Extensions/WpfExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Input; 3 | 4 | namespace Hourglass.Extensions; 5 | 6 | internal static class WpfExtensions 7 | { 8 | private static readonly KeyGestureConverter _keyGestureConverter = new(); 9 | 10 | public static string ToInputGestureText(this KeyGesture keyGesture) => 11 | (string?) _keyGestureConverter.ConvertTo(null, CultureInfo.InvariantCulture, keyGesture, typeof(string)) ?? string.Empty; 12 | } 13 | -------------------------------------------------------------------------------- /Hourglass/Extensions/DisposableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Hourglass.Extensions; 4 | 5 | internal static class DisposableExtensions 6 | { 7 | public static IDisposable CreateDisposable(this Action action) => 8 | new DisposableAction(action); 9 | 10 | private class DisposableAction : IDisposable 11 | { 12 | private readonly Action _action; 13 | 14 | public DisposableAction(Action action) => 15 | _action = action; 16 | 17 | public void Dispose() => 18 | _action(); 19 | } 20 | } -------------------------------------------------------------------------------- /Hourglass/Windows/VisibilityToBooleanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows; 4 | using System.Windows.Data; 5 | 6 | namespace Hourglass.Windows; 7 | 8 | public sealed class VisibilityToBooleanConverter : IValueConverter 9 | { 10 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => 11 | value is Visibility.Visible; 12 | 13 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => 14 | throw new NotSupportedException(); 15 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/HotkeyCharExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Hourglass.Extensions; 2 | 3 | public static class HotkeyCharExtensions 4 | { 5 | private const char HotkeyChar = '_'; 6 | private const string HotkeyString = "_"; 7 | 8 | extension(string text) 9 | { 10 | public string MakeFirstCharHotkey() => 11 | string.IsNullOrWhiteSpace(text) || text[0] == HotkeyChar 12 | ? text 13 | : $"{HotkeyChar}{text}"; 14 | 15 | public string RemoveHotkeyChar() => 16 | text.Replace(HotkeyString, string.Empty); 17 | } 18 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Hourglass.Extensions; 5 | 6 | internal static class ExceptionExtensions 7 | { 8 | public static bool CanBeHandled(this Exception ex) => 9 | ex is not ( 10 | ArgumentNullException or 11 | NullReferenceException or 12 | IndexOutOfRangeException or 13 | OutOfMemoryException or 14 | AccessViolationException or 15 | ThreadAbortException or 16 | StackOverflowException 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogHyperlinkClickedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable all 4 | 5 | namespace KPreisser.UI; 6 | 7 | /// 8 | /// 9 | /// 10 | public class TaskDialogHyperlinkClickedEventArgs : EventArgs 11 | { 12 | /// 13 | /// 14 | /// 15 | /// 16 | internal TaskDialogHyperlinkClickedEventArgs(string hyperlink) 17 | { 18 | Hyperlink = hyperlink; 19 | } 20 | 21 | /// 22 | /// 23 | /// 24 | public string Hyperlink 25 | { 26 | get; 27 | } 28 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogCustomButtonStyle.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable all 2 | 3 | namespace KPreisser.UI; 4 | 5 | /// 6 | /// 7 | /// 8 | public enum TaskDialogCustomButtonStyle 9 | { 10 | /// 11 | /// Custom buttons should be displayed as normal buttons. 12 | /// 13 | Default = 0, 14 | 15 | /// 16 | /// Custom buttons should be displayed as command links. 17 | /// 18 | CommandLinks = 1, 19 | 20 | /// 21 | /// Custom buttons should be displayed as command links, but without an icon. 22 | /// 23 | CommandLinksNoIcon = 2 24 | } -------------------------------------------------------------------------------- /Hourglass/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | using System.Runtime.InteropServices; 8 | using System.Windows; 9 | 10 | [assembly: Guid("83DBAA61-6193-4288-BBB7-BEAEC33FE321")] 11 | [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] 12 | [assembly: ComVisible(false)] 13 | -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogClosingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | // ReSharper disable all 4 | 5 | namespace KPreisser.UI; 6 | 7 | /// 8 | /// 9 | /// 10 | public class TaskDialogClosingEventArgs : CancelEventArgs 11 | { 12 | /// 13 | /// 14 | /// 15 | internal TaskDialogClosingEventArgs(TaskDialogButton closeButton) 16 | { 17 | CloseButton = closeButton; 18 | } 19 | 20 | /// 21 | /// Gets the that is causing the task dialog 22 | /// to close. 23 | /// 24 | public TaskDialogButton CloseButton 25 | { 26 | get; 27 | } 28 | } -------------------------------------------------------------------------------- /Hourglass/Windows/Audio/AudioPlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Media; 3 | 4 | namespace Hourglass.Windows.Audio; 5 | 6 | internal class AudioPlayer: IAudioPlayer 7 | { 8 | private readonly MediaPlayer _mediaPlayer = new(); 9 | 10 | public AudioPlayer(EventHandler stoppedEventHandler) => 11 | _mediaPlayer.MediaEnded += stoppedEventHandler; 12 | 13 | public void Open(string uri) => 14 | _mediaPlayer.Open(new(uri)); 15 | 16 | public void Play() 17 | { 18 | _mediaPlayer.Position = TimeSpan.Zero; 19 | _mediaPlayer.Play(); 20 | } 21 | 22 | public void Stop() 23 | { 24 | _mediaPlayer.Stop(); 25 | _mediaPlayer.Close(); 26 | } 27 | 28 | public void Dispose() => 29 | Stop(); 30 | } 31 | -------------------------------------------------------------------------------- /Hourglass/Extensions/ApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Shell; 3 | 4 | namespace Hourglass.Extensions; 5 | 6 | using Properties; 7 | 8 | internal static class ApplicationExtensions 9 | { 10 | public static void ClearJumpList(this Application? application) 11 | { 12 | if (!Settings.Default.UseJumpList) 13 | { 14 | return; 15 | } 16 | 17 | try 18 | { 19 | if (application is null) 20 | { 21 | return; 22 | } 23 | 24 | var jumpList = JumpList.GetJumpList(application); 25 | 26 | jumpList?.JumpItems.Clear(); 27 | jumpList?.Apply(); 28 | } 29 | catch 30 | { 31 | // Ignore. 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogButtonClickedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable all 4 | 5 | namespace KPreisser.UI; 6 | 7 | /// 8 | /// 9 | /// 10 | public class TaskDialogButtonClickedEventArgs : EventArgs 11 | { 12 | /// 13 | /// 14 | /// 15 | internal TaskDialogButtonClickedEventArgs() 16 | { 17 | } 18 | 19 | /// 20 | /// Gets or sets a value that indicates if the dialog should not be closed 21 | /// after the event handler returns. 22 | /// 23 | /// 24 | /// When you don't set this property to true, the 25 | /// event will occur afterwards. 26 | /// 27 | public bool CancelClose 28 | { 29 | get; 30 | set; 31 | } 32 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogIconHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | // ReSharper disable all 5 | 6 | namespace KPreisser.UI; 7 | 8 | /// 9 | /// 10 | /// 11 | public class TaskDialogIconHandle : TaskDialogIcon 12 | { 13 | /// 14 | /// 15 | /// 16 | /// 17 | public TaskDialogIconHandle(IntPtr iconHandle) 18 | { 19 | IconHandle = iconHandle; 20 | } 21 | 22 | #if !NET_STANDARD 23 | /// 24 | /// 25 | /// 26 | /// 27 | public TaskDialogIconHandle(Icon icon) 28 | : this(icon?.Handle ?? default) 29 | { 30 | } 31 | #endif 32 | 33 | /// 34 | /// 35 | /// 36 | public IntPtr IconHandle 37 | { 38 | get; 39 | } 40 | } -------------------------------------------------------------------------------- /Hourglass/Serialization/UpdateInfo.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | /// 10 | /// The representation of the latest version of the app used for XML serialization. 11 | /// 12 | public sealed class UpdateInfo 13 | { 14 | /// 15 | /// Gets or sets the latest version of the app. 16 | /// 17 | public string LatestVersion { get; set; } = null!; 18 | 19 | /// 20 | /// Gets or sets the URL to download the update to the latest version of the app. 21 | /// 22 | public string UpdateUrl { get; set; } = null!; 23 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/NotifyPropertyChangedExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Hourglass.Extensions; 5 | 6 | public static class NotifyPropertyChangedExtensions 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | public static void Notify(this PropertyChangedEventHandler? propertyChanged, T sender, [CallerMemberName] string propertyName = "") 10 | where T: INotifyPropertyChanged => 11 | propertyChanged?.Invoke(sender, new(propertyName)); 12 | 13 | public static void Notify(this PropertyChangedEventHandler? propertyChanged, T sender, string firstPropertyName, params string[] propertyNames) 14 | where T : INotifyPropertyChanged 15 | { 16 | if (propertyChanged is null) 17 | { 18 | return; 19 | } 20 | 21 | propertyChanged.Notify(sender, firstPropertyName); 22 | 23 | foreach (var propertyName in propertyNames) 24 | { 25 | propertyChanged.Notify(sender, propertyName); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Hourglass/Resources/License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2021 Chris Dziemborowicz, 2024-2025 Ivan Ivon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Hourglass/Windows/Audio/AudioPlayerSelector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Hourglass.Windows.Audio; 4 | 5 | internal static class AudioPlayerSelector 6 | { 7 | private static Func? _createFunc; 8 | 9 | public static IAudioPlayer Create(EventHandler stoppedEventHandler) 10 | { 11 | if (_createFunc is not null) 12 | { 13 | return _createFunc(stoppedEventHandler); 14 | } 15 | 16 | IAudioPlayer player; 17 | 18 | try 19 | { 20 | player = CreateNAudioPlayer(stoppedEventHandler); 21 | _createFunc = CreateNAudioPlayer; 22 | } 23 | catch 24 | { 25 | player = CreateAudioPlayer(stoppedEventHandler); 26 | _createFunc = CreateAudioPlayer; 27 | } 28 | 29 | return player; 30 | 31 | static IAudioPlayer CreateAudioPlayer(EventHandler stoppedEventHandler) => 32 | new AudioPlayer(stoppedEventHandler); 33 | 34 | static IAudioPlayer CreateNAudioPlayer(EventHandler stoppedEventHandler) => 35 | new NAudioPlayer(stoppedEventHandler); 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2021 Chris Dziemborowicz, 2024-2025 Ivan Ivon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Hourglass.Bundle/Bundle.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Hourglass/Extensions/IRestorableWindow.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System.Windows; 10 | 11 | using Windows; 12 | 13 | /// 14 | /// An interface for windows whose size, position, and state of windows can be saved and restored. 15 | /// 16 | public interface IRestorableWindow 17 | { 18 | /// 19 | /// Gets the for the window persisted in the settings. 20 | /// 21 | WindowSize PersistedSize { get; } 22 | 23 | /// 24 | /// Gets or sets the before the window was minimized. 25 | /// 26 | WindowState RestoreWindowState { get; set; } 27 | 28 | /// 29 | /// Gets or sets a value indicating whether the window is in full-screen mode. 30 | /// 31 | bool IsFullScreen { get; set; } 32 | } -------------------------------------------------------------------------------- /Hourglass.Bundle/MIT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} 2 | {\*\generator Riched20 10.0.19041}\viewkind4\uc1 3 | \pard\sa200\sl276\slmult1\f0\fs22\lang9 Copyright (c) 2021 Chris Dziemborowicz, 2024-2025 Ivan Ivon\par 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\par 7 | } 8 | -------------------------------------------------------------------------------- /Hourglass/Windows/Audio/NAudioPlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using NAudio.Vorbis; 4 | using NAudio.Wave; 5 | 6 | namespace Hourglass.Windows.Audio; 7 | 8 | internal class NAudioPlayer : IAudioPlayer 9 | { 10 | private readonly WaveOutEvent _waveOutEvent = new(); 11 | 12 | private WaveStream? _audioFile; 13 | 14 | public NAudioPlayer(EventHandler stoppedEventHandler) => 15 | _waveOutEvent.PlaybackStopped += (s, e) => stoppedEventHandler(s, e); 16 | 17 | public void Open(string uri) 18 | { 19 | _audioFile?.Dispose(); 20 | _audioFile = null; 21 | _audioFile = IsOgg() 22 | ? new VorbisWaveReader(uri) 23 | : new AudioFileReader(uri); 24 | 25 | _waveOutEvent.Init(_audioFile); 26 | 27 | bool IsOgg() => 28 | uri.EndsWith(".ogg", StringComparison.OrdinalIgnoreCase); 29 | } 30 | 31 | public void Play() 32 | { 33 | _audioFile!.Position = 0; 34 | _waveOutEvent.Play(); 35 | } 36 | 37 | public void Stop() 38 | { 39 | _waveOutEvent.Stop(); 40 | 41 | _audioFile?.Dispose(); 42 | _audioFile = null; 43 | } 44 | 45 | public void Dispose() 46 | { 47 | Stop(); 48 | 49 | _waveOutEvent.Dispose(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Hourglass/Serialization/TimerStartInfo.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | using Parsing; 10 | using Timing; 11 | 12 | /// 13 | /// The representation of a used for XML serialization. 14 | /// 15 | public sealed class TimerStartInfo 16 | { 17 | /// 18 | /// Gets or sets the . 19 | /// 20 | public TimerStartToken TimerStartToken { get; set; } = null!; 21 | 22 | /// 23 | /// Returns a for a . 24 | /// 25 | /// A . 26 | /// The for the . 27 | public static TimerStartInfo? FromTimerStart(TimerStart? timerStart) 28 | { 29 | return timerStart?.ToTimerStartInfo(); 30 | } 31 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogProgressBarState.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable all 2 | 3 | namespace KPreisser.UI; 4 | 5 | /// 6 | /// 7 | /// 8 | public enum TaskDialogProgressBarState : int 9 | { 10 | /// 11 | /// Shows a regular progress bar. 12 | /// 13 | Normal = 0, 14 | 15 | /// 16 | /// Shows a paused (yellow) progress bar. 17 | /// 18 | Paused, 19 | 20 | /// 21 | /// Shows an error (red) progress bar. 22 | /// 23 | Error, 24 | 25 | /// 26 | /// Shows a marquee progress bar. 27 | /// 28 | Marquee, 29 | 30 | /// 31 | /// Shows a marquee progress bar where the marquee animation is paused. 32 | /// 33 | /// 34 | /// For example, if you switch from to 35 | /// while the dialog is shown, the 36 | /// marquee animation will stop. 37 | /// 38 | MarqueePaused, 39 | 40 | /// 41 | /// The progress bar will not be displayed. 42 | /// 43 | /// 44 | /// Note that while the dialog is showing, you cannot switch from 45 | /// to any other state, and vice versa. 46 | /// 47 | None 48 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogStandardIcon.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable all 2 | 3 | namespace KPreisser.UI; 4 | 5 | /// 6 | /// 7 | /// 8 | public enum TaskDialogStandardIcon : int 9 | { 10 | /// 11 | /// 12 | /// 13 | None = 0, 14 | 15 | /// 16 | /// 17 | /// 18 | Information = ushort.MaxValue - 2, // TD_INFORMATION_ICON 19 | 20 | /// 21 | /// 22 | /// 23 | Warning = ushort.MaxValue, // TD_WARNING_ICON 24 | 25 | /// 26 | /// 27 | /// 28 | Error = ushort.MaxValue - 1, // TD_ERROR_ICON 29 | 30 | /// 31 | /// 32 | /// 33 | SecurityShield = ushort.MaxValue - 3, // TD_SHIELD_ICON 34 | 35 | /// 36 | /// 37 | /// 38 | SecurityShieldBlueBar = ushort.MaxValue - 4, 39 | 40 | /// 41 | /// 42 | /// 43 | SecurityShieldGrayBar = ushort.MaxValue - 8, 44 | 45 | /// 46 | /// 47 | /// 48 | SecurityWarningYellowBar = ushort.MaxValue - 5, 49 | 50 | /// 51 | /// 52 | /// 53 | SecurityErrorRedBar = ushort.MaxValue - 6, 54 | 55 | /// 56 | /// 57 | /// 58 | SecuritySuccessGreenBar = ushort.MaxValue - 7, 59 | } -------------------------------------------------------------------------------- /Hourglass/Managers/SettingsManager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using Properties; 10 | 11 | // ReSharper disable ExceptionNotDocumented 12 | 13 | /// 14 | /// Manages default settings. 15 | /// 16 | public sealed class SettingsManager : Manager 17 | { 18 | /// 19 | /// Singleton instance of the class. 20 | /// 21 | public static readonly SettingsManager Instance = new(); 22 | 23 | /// 24 | /// Prevents a default instance of the class from being created. 25 | /// 26 | private SettingsManager() 27 | { 28 | } 29 | 30 | /// 31 | public override void Initialize() 32 | { 33 | if (Settings.Default.UpgradeRequired) 34 | { 35 | Settings.Default.Upgrade(); 36 | Settings.Default.UpgradeRequired = false; 37 | Settings.Default.Save(); 38 | } 39 | } 40 | 41 | /// 42 | public override void Persist() 43 | { 44 | Settings.Default.Save(); 45 | } 46 | } -------------------------------------------------------------------------------- /Hourglass/ngen-Hourglass.bat: -------------------------------------------------------------------------------- 1 | :: Installs/uninstalls Hourglass and its dependencies in/from the native image cache. 2 | :: Run as an Administrator. 3 | 4 | @echo off 5 | 6 | set scriptName=%~nx0 7 | set command=%~1 8 | set faq=https://github.com/i2van/hourglass/blob/main/FAQ.md#how-to-speed-up-the-portable-hourglass-startup 9 | 10 | :: Check permissions. 11 | net session >nul 2>&1 12 | if not %errorlevel%==0 goto NO_PERMISSIONS 13 | 14 | :: Params. 15 | if "%command%"=="install" goto EXECUTE 16 | if "%command%"=="uninstall" goto EXECUTE 17 | 18 | :: Usage. 19 | echo Usage: %scriptName% [install^|uninstall] 20 | echo. 21 | echo ^> %scriptName% install 22 | echo Generates the Hourglass native image and its dependencies and installs in the native image cache. 23 | echo. 24 | echo ^> %scriptName% uninstall 25 | echo Deletes the native images of the Hourglass and its dependencies from the native image cache. 26 | echo. 27 | echo FAQ: %faq% 28 | exit /b 1 29 | 30 | :EXECUTE 31 | 32 | :: .NET Framework version. 33 | set netVersion=4.0.30319 34 | 35 | set nAudio=%~dp0Hourglass.NAudio.dll 36 | set netPath=%WINDIR%\Microsoft.NET\Framework 37 | set ngenPath=%netPath%64 38 | 39 | if not exist "%ngenPath%" set ngenPath=%netPath% 40 | 41 | echo on 42 | 43 | "%ngenPath%\v%netVersion%\ngen.exe" %~1 "%~dp0Hourglass.exe" > nul || goto EXIT 44 | @if not exist "%nAudio%" goto EXIT 45 | "%ngenPath%\v%netVersion%\ngen.exe" %~1 "%nAudio%" || goto EXIT 46 | 47 | :EXIT 48 | 49 | @exit /b %errorlevel% 50 | 51 | :NO_PERMISSIONS 52 | 53 | echo Please run %scriptName% as an Administrator: %faq% 54 | exit /b 1 55 | -------------------------------------------------------------------------------- /Hourglass/Extensions/WindowsExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Linq; 11 | using System.Management; 12 | 13 | /// 14 | /// Provides utility methods for interacting with the Windows environment. 15 | /// 16 | public static class WindowsExtensions 17 | { 18 | /// 19 | /// Shuts down the computer. 20 | /// 21 | public static void ShutDown() 22 | { 23 | try 24 | { 25 | ManagementClass os = new("Win32_OperatingSystem"); 26 | os.Get(); 27 | os.Scope.Options.EnablePrivileges = true; 28 | 29 | ManagementBaseObject parameters = os.GetMethodParameters("Win32Shutdown"); 30 | parameters["Flags"] = "1"; // Shut down 31 | parameters["Reserved"] = "0"; 32 | 33 | foreach (ManagementObject obj in os.GetInstances().Cast()) 34 | { 35 | obj.InvokeMethod("Win32Shutdown", parameters, null /* options */); 36 | } 37 | } 38 | catch (Exception ex) when (ex.CanBeHandled()) 39 | { 40 | // Ignored. 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/FrameworkElementExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System.Windows; 10 | using System.Windows.Input; 11 | 12 | /// 13 | /// Provides extensions methods for the class. 14 | /// 15 | public static class FrameworkElementExtensions 16 | { 17 | /// 18 | /// Removes focus from the . 19 | /// 20 | /// A . 21 | /// A value indicating whether the focus was removed from the element. 22 | public static bool Unfocus(this FrameworkElement element) 23 | { 24 | if (!element.IsFocused) 25 | { 26 | return false; 27 | } 28 | 29 | FrameworkElement parent = (FrameworkElement)element.Parent; 30 | while (parent is not null && !((IInputElement)parent).Focusable) 31 | { 32 | parent = (FrameworkElement)parent.Parent; 33 | } 34 | 35 | DependencyObject scope = FocusManager.GetFocusScope(element); 36 | FocusManager.SetFocusedElement(scope, parent); 37 | 38 | return true; 39 | } 40 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/EnvironmentExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | 11 | /// 12 | /// Provides information about the current environment and platform. 13 | /// 14 | public static class EnvironmentExtensions 15 | { 16 | /// 17 | /// Gets a value indicating whether the current environment and platform is Windows 10 or newer. 18 | /// 19 | public static bool IsWindows10OrNewer => 20 | Environment.OSVersion.Platform == PlatformID.Win32NT 21 | && Environment.OSVersion.Version.Major >= 10; 22 | 23 | /// 24 | /// Gets a value indicating whether the current environment and platform is Windows 10 with a specified build 25 | /// or newer. 26 | /// 27 | /// A minimum Windows 10 build to use as a threshold. 28 | /// A value indicating whether the current environment and platform is Windows 10 with a specified 29 | /// build or newer. 30 | public static bool IsWindows10BuildOrNewer(int build) 31 | { 32 | return Environment.OSVersion.Platform == PlatformID.Win32NT 33 | && Environment.OSVersion.Version.Major >= 10 34 | && Environment.OSVersion.Version.Build >= build; 35 | } 36 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin10.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | internal sealed class VirtualDesktopWin10(ImmersiveShellProvider immersiveShellProvider) 9 | : VirtualDesktop(immersiveShellProvider) 10 | { 11 | protected override Guid GetCurrentDesktopId() => 12 | VirtualDesktopManagerInternal!.GetCurrentDesktop().GetId(); 13 | 14 | [ComImport] 15 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 16 | [Guid("FF72FFDD-BE7E-43FC-9C03-AD81681E88E4")] 17 | internal interface IVirtualDesktop 18 | { 19 | bool IsViewVisible(IntPtr view); 20 | Guid GetId(); 21 | } 22 | 23 | [ComImport] 24 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 25 | [Guid("F31574D6-B682-4CDC-BD56-1827860ABEC6")] 26 | internal interface IVirtualDesktopManagerInternal 27 | { 28 | int GetCount(); 29 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 30 | bool CanViewMoveDesktops(IntPtr view); 31 | IVirtualDesktop GetCurrentDesktop(); 32 | void GetDesktops(out IntPtr desktops); 33 | 34 | [PreserveSig] 35 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 36 | 37 | void SwitchDesktop(IVirtualDesktop desktop); 38 | IVirtualDesktop CreateDesktop(); 39 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 40 | IVirtualDesktop FindDesktop(ref Guid desktopId); 41 | } 42 | } -------------------------------------------------------------------------------- /Hourglass/Managers/NotificationAreaIconManager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using Windows; 10 | 11 | /// 12 | /// Manages the . 13 | /// 14 | public sealed class NotificationAreaIconManager : Manager 15 | { 16 | /// 17 | /// Singleton instance of the class. 18 | /// 19 | public static readonly NotificationAreaIconManager Instance = new(); 20 | 21 | /// 22 | /// Prevents a default instance of the class from being created. 23 | /// 24 | private NotificationAreaIconManager() 25 | { 26 | } 27 | 28 | /// 29 | /// Gets the icon for the app in the notification area of the taskbar. 30 | /// 31 | public NotificationAreaIcon NotifyIcon { get; private set; } = null!; 32 | 33 | /// 34 | public override void Initialize() 35 | { 36 | NotifyIcon = new(); 37 | } 38 | 39 | /// 40 | protected override void Dispose(bool disposing) 41 | { 42 | if (Disposed) 43 | { 44 | return; 45 | } 46 | 47 | if (disposing) 48 | { 49 | NotifyIcon.Dispose(); 50 | } 51 | 52 | base.Dispose(disposing); 53 | } 54 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogButtons.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable all 4 | 5 | namespace KPreisser.UI; 6 | 7 | /// 8 | /// 9 | /// 10 | [Flags] 11 | public enum TaskDialogButtons : int 12 | { 13 | /// 14 | /// 15 | /// 16 | None = 0, 17 | 18 | /// 19 | /// 20 | /// 21 | OK = 1 << 0, 22 | 23 | /// 24 | /// 25 | /// 26 | Yes = 1 << 1, 27 | 28 | /// 29 | /// 30 | /// 31 | No = 1 << 2, 32 | 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// Note: Adding a Cancel button will automatically add a close button 38 | /// to the task dialog's title bar and will allow to close the dialog by 39 | /// pressing ESC or Alt+F4 (just as if you enabled 40 | /// ). 41 | /// 42 | Cancel = 1 << 3, 43 | 44 | /// 45 | /// 46 | /// 47 | Retry = 1 << 4, 48 | 49 | /// 50 | /// 51 | /// 52 | Close = 1 << 5, 53 | 54 | /// 55 | /// 56 | /// 57 | Abort = 1 << 16, 58 | 59 | /// 60 | /// 61 | /// 62 | Ignore = 1 << 17, 63 | 64 | /// 65 | /// 66 | /// 67 | TryAgain = 1 << 18, 68 | 69 | /// 70 | /// 71 | /// 72 | Continue = 1 << 19, 73 | 74 | /// 75 | /// 76 | /// 77 | /// 78 | /// Note: Clicking this button will not close the dialog, but will raise the 79 | /// event. 80 | /// 81 | Help = 1 << 20 82 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/ImmersiveShellProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | internal sealed class ImmersiveShellProvider : IDisposable 9 | { 10 | private IServiceProvider10? _serviceProvider; 11 | 12 | public ImmersiveShellProvider() 13 | { 14 | try 15 | { 16 | _serviceProvider = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(new("C2F03A33-21F5-47FA-B4BB-156362A2F239") /* CLSID_ImmersiveShell */)); 17 | } 18 | catch 19 | { 20 | // Ignore. 21 | } 22 | } 23 | 24 | public T? QueryService(Guid service) where T : class 25 | { 26 | object? obj = null; 27 | try 28 | { 29 | obj = _serviceProvider?.QueryService(service, typeof(T).GUID); 30 | return (T?)obj; 31 | } 32 | catch 33 | { 34 | if (obj is not null) 35 | { 36 | Marshal.FinalReleaseComObject(obj); 37 | } 38 | 39 | return null; 40 | } 41 | } 42 | 43 | public void Dispose() 44 | { 45 | if (_serviceProvider is not null) 46 | { 47 | Marshal.FinalReleaseComObject(_serviceProvider); 48 | _serviceProvider = null; 49 | } 50 | } 51 | 52 | [ComImport] 53 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 54 | [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] 55 | internal interface IServiceProvider10 56 | { 57 | [return: MarshalAs(UnmanagedType.IUnknown)] 58 | object QueryService(ref Guid service, ref Guid riid); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Hourglass/Managers/Manager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using System; 10 | 11 | /// 12 | /// A base class for singleton manager classes. 13 | /// 14 | public abstract class Manager : IDisposable 15 | { 16 | /// 17 | /// Gets a value indicating whether this object has been disposed. 18 | /// 19 | public bool Disposed { get; private set; } 20 | 21 | /// 22 | /// Initializes the class. 23 | /// 24 | public virtual void Initialize() 25 | { 26 | } 27 | 28 | /// 29 | /// Persists the state of the class. 30 | /// 31 | public virtual void Persist() 32 | { 33 | } 34 | 35 | /// 36 | /// Disposes the manager. 37 | /// 38 | public void Dispose() 39 | { 40 | Dispose(true); 41 | GC.SuppressFinalize(this); 42 | } 43 | 44 | /// 45 | /// Disposes the manager. 46 | /// 47 | /// A value indicating whether this method was invoked by an explicit call to . 49 | protected virtual void Dispose(bool disposing) 50 | { 51 | if (Disposed) 52 | { 53 | return; 54 | } 55 | 56 | Disposed = true; 57 | 58 | if (disposing) 59 | { 60 | // Do nothing 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Hourglass/Properties/App.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The simple countdown timer for Windows. 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | PerMonitorV2 33 | true/PM 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Hourglass/Serialization/ThemeInfoList.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | using System.Collections.Generic; 10 | 11 | /// 12 | /// A list of objects used for XML serialization. 13 | /// 14 | public sealed class ThemeInfoList : List 15 | { 16 | /// 17 | /// Initializes a new instance of the class that is empty and has the default 18 | /// initial capacity. 19 | /// 20 | public ThemeInfoList() 21 | { 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class that contains elements copied from the 26 | /// specified collection and has sufficient capacity to accommodate the number of elements copied. 27 | /// 28 | /// The collection whose elements are copied to the new list. 29 | public ThemeInfoList(IEnumerable collection) 30 | : base(collection) 31 | { 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class that is empty and has the specified 36 | /// initial capacity. 37 | /// 38 | /// The number of elements that the new list can initially store. 39 | public ThemeInfoList(int capacity) 40 | : base(capacity) 41 | { 42 | } 43 | } -------------------------------------------------------------------------------- /Hourglass/Serialization/TimerInfoList.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | using System.Collections.Generic; 10 | 11 | /// 12 | /// A list of objects used for XML serialization. 13 | /// 14 | public sealed class TimerInfoList : List 15 | { 16 | /// 17 | /// Initializes a new instance of the class that is empty and has the default 18 | /// initial capacity. 19 | /// 20 | public TimerInfoList() 21 | { 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class that contains elements copied from the 26 | /// specified collection and has sufficient capacity to accommodate the number of elements copied. 27 | /// 28 | /// The collection whose elements are copied to the new list. 29 | public TimerInfoList(IEnumerable collection) 30 | : base(collection) 31 | { 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class that is empty and has the specified 36 | /// initial capacity. 37 | /// 38 | /// The number of elements that the new list can initially store. 39 | public TimerInfoList(int capacity) 40 | : base(capacity) 41 | { 42 | } 43 | } -------------------------------------------------------------------------------- /Hourglass/Serialization/TimerStartInfoList.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | using System.Collections.Generic; 10 | 11 | /// 12 | /// A list of objects used for XML serialization. 13 | /// 14 | public sealed class TimerStartInfoList : List 15 | { 16 | /// 17 | /// Initializes a new instance of the class that is empty and has the default 18 | /// initial capacity. 19 | /// 20 | public TimerStartInfoList() 21 | { 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class that contains elements copied from 26 | /// the specified collection and has sufficient capacity to accommodate the number of elements copied. 27 | /// 28 | /// The collection whose elements are copied to the new list. 29 | public TimerStartInfoList(IEnumerable collection) 30 | : base(collection) 31 | { 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class that is empty and has the 36 | /// specified initial capacity. 37 | /// 38 | /// The number of elements that the new list can initially store. 39 | public TimerStartInfoList(int capacity) 40 | : base(capacity) 41 | { 42 | } 43 | } -------------------------------------------------------------------------------- /Hourglass.Test/Hourglass.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | {CBE74854-0344-4606-A854-BFA719896A98} 4 | net481 5 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 6 | preview 7 | enable 8 | bin\$(Configuration)\ 9 | 10 | 11 | Hourglass.Test 12 | Hourglass.Test 13 | Chris Dziemborowicz, Ivan Ivon 14 | en-US 15 | Copyright © 2021 Chris Dziemborowicz, 2024-$([System.DateTime]::Now.Year) Ivan Ivon 16 | $(Version) 17 | $(Version) 18 | 19 | 20 | trx%3bLogFileName=$(MSBuildProjectName).trx 21 | $(MSBuildThisFileDirectory)/TestResults/$(TargetFramework) 22 | 23 | 24 | full 25 | 26 | 27 | pdbonly 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Tools/ResourceHacker/ReadMe.txt: -------------------------------------------------------------------------------- 1 | Resource Hacker™ 2 | Copyright © 1998-2025 Angus Johnson 3 | Freeware - no nags, no ads and fully functional. 4 | 5 | 6 | Overview: 7 | 8 | Resource Hacker™ is a resource editor for 32bit and 64bit Windows® applications. It's both a resource compiler (for *.rc files), and a decompiler - enabling viewing and editing of resources in executables (*.exe; *.dll; *.scr; etc) and compiled resource libraries (*.res, *.mui). While Resource Hacker™ is primarily a GUI application, it also provides many options for compiling and decompiling resources from the command-line. 9 | 10 | Licence to Use - Terms and Conditions: 11 | 12 | This Resource Hacker™ software is released as freeware provided that you agree to the following terms and conditions: 13 | 1. This software is not used for purposes that will infringe third-party license agreements. 14 | 2. This software may only be redistributed under the following conditions: 15 | a. It is redistributed in its entirety, either in ZIP or setup EXE formats. 16 | b. It is not bundled with software that will likely infringe third-party license agreements. 17 | 18 | DISCLAIMER: A user of this Resource Hacker™ software acknowledges that he or she is receiving this software on an "as is" basis and the user is not relying on the accuracy or functionality of the software for any purpose. The user further acknowledges that any use of this software will be at the user's own risk and the copyright owner accepts no responsibility whatsoever arising from the use or application of the software. 19 | 20 | The above licence terms constitute "copyright management information" within the meaning of Section 1202 of Title 17 of the United States Code and must not be altered or removed from the licensed works. Their alteration or removal from the licensed works, and the distribution of licensed works without all the above licence terms in an unaltered way, may contravene Section 1202 and give rise civil and/or criminal consequences. 21 | -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogResult.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable all 2 | 3 | namespace KPreisser.UI; 4 | 5 | /// 6 | /// 7 | /// 8 | public enum TaskDialogResult : int 9 | { 10 | /// 11 | /// 12 | /// 13 | None = 0, 14 | 15 | /// 16 | /// 17 | /// 18 | OK = TaskDialogNativeMethods.IDOK, 19 | 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// Note: Adding a Cancel button will automatically add a close button 25 | /// to the task dialog's title bar and will allow to close the dialog by 26 | /// pressing ESC or Alt+F4 (just as if you enabled 27 | /// ). 28 | /// 29 | Cancel = TaskDialogNativeMethods.IDCANCEL, 30 | 31 | /// 32 | /// 33 | /// 34 | Abort = TaskDialogNativeMethods.IDABORT, 35 | 36 | /// 37 | /// 38 | /// 39 | Retry = TaskDialogNativeMethods.IDRETRY, 40 | 41 | /// 42 | /// 43 | /// 44 | Ignore = TaskDialogNativeMethods.IDIGNORE, 45 | 46 | /// 47 | /// 48 | /// 49 | Yes = TaskDialogNativeMethods.IDYES, 50 | 51 | /// 52 | /// 53 | /// 54 | No = TaskDialogNativeMethods.IDNO, 55 | 56 | /// 57 | /// 58 | /// 59 | Close = TaskDialogNativeMethods.IDCLOSE, 60 | 61 | /// 62 | /// 63 | /// 64 | /// 65 | /// Note: Clicking this button will not close the dialog, but will raise the 66 | /// event. 67 | /// 68 | Help = TaskDialogNativeMethods.IDHELP, 69 | 70 | /// 71 | /// 72 | /// 73 | TryAgain = TaskDialogNativeMethods.IDTRYAGAIN, 74 | 75 | /// 76 | /// 77 | /// 78 | Continue = TaskDialogNativeMethods.IDCONTINUE 79 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/ColorExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Runtime.CompilerServices; 11 | using System.Windows.Media; 12 | 13 | /// 14 | /// Provides utility methods for the struct. 15 | /// 16 | public static class ColorExtensions 17 | { 18 | /// 19 | /// Converts a to an representation. 20 | /// 21 | /// A . 22 | /// An for . 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public static int ToInt(this Color color) 25 | { 26 | return (color.R << 0) | (color.G << 8) | (color.B << 16); 27 | } 28 | 29 | /// 30 | /// Converts a representation of a into a . 31 | /// 32 | /// A representation of a . 33 | /// A . 34 | /// is 35 | public static Color FromString(string colorString) 36 | { 37 | if (string.IsNullOrWhiteSpace(colorString)) 38 | { 39 | throw new ArgumentNullException(nameof(colorString)); 40 | } 41 | 42 | object? color = ColorConverter.ConvertFromString(colorString); 43 | 44 | return color is null ? throw new ArgumentNullException(nameof(colorString)) : (Color)color; 45 | } 46 | } -------------------------------------------------------------------------------- /Hourglass/Serialization/WindowSizeInfo.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | using System.Windows; 10 | 11 | using Windows; 12 | 13 | /// 14 | /// The representation of a used for XML serialization. 15 | /// 16 | public sealed class WindowSizeInfo 17 | { 18 | /// 19 | /// Gets or sets the size and location of the window before being either minimized or maximized. 20 | /// 21 | public Rect RestoreBounds { get; set; } 22 | 23 | /// 24 | /// Gets or sets a value that indicates whether the window is restored, minimized, or maximized. 25 | /// 26 | public WindowState WindowState { get; set; } 27 | 28 | /// 29 | /// Gets or sets the window's before the window was minimized. 30 | /// 31 | public WindowState RestoreWindowState { get; set; } 32 | 33 | /// 34 | /// Gets or sets a value indicating whether the window is in full-screen mode. 35 | /// 36 | public bool IsFullScreen { get; set; } 37 | 38 | /// 39 | /// Returns a for the specified , or null if the 40 | /// specified is null. 41 | /// 42 | /// A . 43 | /// A for the specified , or null if the specified is null. 44 | public static WindowSizeInfo? FromWindowSize(WindowSize? windowSize) 45 | { 46 | return windowSize?.ToWindowSizeInfo(); 47 | } 48 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/CultureInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Globalization; 11 | using System.Text.RegularExpressions; 12 | 13 | // ReSharper disable ExceptionNotDocumented 14 | 15 | /// 16 | /// Provides extensions methods for the class and the related interface. 18 | /// 19 | public static class CultureInfoExtensions 20 | { 21 | /// An . 22 | extension(IFormatProvider provider) 23 | { 24 | /// 25 | /// Returns a value indicating whether an prefers the month-day-year ordering in 26 | /// date representations. 27 | /// 28 | /// A value indicating whether the specified prefers the month-day-year 29 | /// ordering in date representations. 30 | public bool IsMonthFirst => 31 | Regex.IsMatch(provider.GetShortDatePattern(), @"^.*M.*d.*y.*$"); 32 | 33 | /// 34 | /// Returns a value indicating whether an prefers the year-month-day ordering in 35 | /// date representations. 36 | /// 37 | /// A value indicating whether the specified prefers the year-month-day 38 | /// ordering in date representations. 39 | public bool IsYearFirst => 40 | Regex.IsMatch(provider.GetShortDatePattern(), @"^.*y.*M.*d.*$"); 41 | 42 | private string GetShortDatePattern() => 43 | ((DateTimeFormatInfo?)provider.GetFormat(typeof(DateTimeFormatInfo)))!.ShortDatePattern; 44 | } 45 | } -------------------------------------------------------------------------------- /Hourglass/Windows/RegexMatchConverter.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Windows; 8 | 9 | using System; 10 | using System.Globalization; 11 | using System.Text.RegularExpressions; 12 | using System.Windows.Data; 13 | 14 | // ReSharper disable ExceptionNotDocumented 15 | 16 | /// 17 | /// Matches the string representation of objects against a regular expression pattern. 18 | /// 19 | public sealed class RegexMatchConverter : IValueConverter 20 | { 21 | /// 22 | /// Converts a value. 23 | /// 24 | /// The value produced by the binding source. 25 | /// The type of the binding target property. 26 | /// The converter parameter to use. 27 | /// The culture to use in the converter. 28 | /// A value indicating whether the value produced by the binding source matches the regular expression passed as the converter parameter. 29 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 30 | { 31 | return Regex.IsMatch(value!.ToString(), (string)parameter!); 32 | } 33 | 34 | /// 35 | /// Converts a value back. This method is not implemented. 36 | /// 37 | /// The value that is produced by the binding target. 38 | /// The type to convert to. 39 | /// The converter parameter to use. 40 | /// The culture to use in the converter. 41 | /// Nothing. This method is not implemented. 42 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 43 | { 44 | throw new NotSupportedException(); 45 | } 46 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialog.WindowSubclassHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable all 4 | 5 | namespace KPreisser.UI; 6 | 7 | public partial class TaskDialog 8 | { 9 | private class WindowSubclassHandler : UI.WindowSubclassHandler 10 | { 11 | private readonly TaskDialog _taskDialog; 12 | 13 | private bool _processedShowWindowMessage; 14 | 15 | public WindowSubclassHandler(TaskDialog taskDialog) 16 | : base(taskDialog?._hwndDialog ?? throw new ArgumentNullException(nameof(taskDialog))) 17 | { 18 | _taskDialog = taskDialog; 19 | } 20 | 21 | protected override unsafe IntPtr WndProc(int msg, IntPtr wParam, IntPtr lParam) 22 | { 23 | switch (msg) 24 | { 25 | case TaskDialogNativeMethods.WM_WINDOWPOSCHANGED: 26 | IntPtr result = base.WndProc(msg, wParam, lParam); 27 | 28 | ref TaskDialogNativeMethods.WINDOWPOS windowPos = 29 | ref *(TaskDialogNativeMethods.WINDOWPOS*)lParam; 30 | 31 | if ((windowPos.flags & TaskDialogNativeMethods.WINDOWPOS_FLAGS.SWP_SHOWWINDOW) == 32 | TaskDialogNativeMethods.WINDOWPOS_FLAGS.SWP_SHOWWINDOW && 33 | !_processedShowWindowMessage) 34 | { 35 | _processedShowWindowMessage = true; 36 | 37 | // The task dialog window has been shown for the first time. 38 | _taskDialog.OnShown(EventArgs.Empty); 39 | } 40 | 41 | return result; 42 | 43 | case ContinueButtonClickHandlingMessage: 44 | // We received the message which we posted earlier when 45 | // handling a TDN_BUTTON_CLICKED notification, so we should 46 | // no longer ignore such notifications. 47 | _taskDialog._ignoreButtonClickedNotifications = false; 48 | 49 | // Do not forward the message to the base class. 50 | return IntPtr.Zero; 51 | 52 | default: 53 | return base.WndProc(msg, wParam, lParam); 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Hourglass/Parsing/EmptyTimeToken.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Parsing; 8 | 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Text.RegularExpressions; 12 | 13 | /// 14 | /// Represents an unspecified time. 15 | /// 16 | public sealed class EmptyTimeToken : TimeToken 17 | { 18 | /// 19 | public override bool IsValid => true; 20 | 21 | /// 22 | public override DateTime ToDateTime(DateTime minDate, DateTime datePart) 23 | { 24 | ThrowIfNotValid(); 25 | 26 | return datePart.Date; 27 | } 28 | 29 | /// 30 | public override string ToString(IFormatProvider provider) 31 | { 32 | return string.Empty; 33 | } 34 | 35 | /// 36 | /// Parses strings. 37 | /// 38 | public new sealed class Parser : TimeToken.Parser 39 | { 40 | /// 41 | /// Singleton instance of the class. 42 | /// 43 | public static readonly Parser Instance = new(); 44 | 45 | /// 46 | /// Prevents a default instance of the class from being created. 47 | /// 48 | private Parser() 49 | { 50 | } 51 | 52 | /// 53 | public override bool IsCompatibleWith(DateToken.Parser dateTokenParser) 54 | { 55 | return dateTokenParser is not EmptyDateToken.Parser; 56 | } 57 | 58 | /// 59 | public override IEnumerable GetPatterns(IFormatProvider provider) 60 | { 61 | yield return string.Empty; 62 | } 63 | 64 | /// 65 | protected override TimeToken ParseInternal(Match match, IFormatProvider provider) 66 | { 67 | return new EmptyTimeToken(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Hourglass/Windows/ColorControl.xaml: -------------------------------------------------------------------------------- 1 | 9 | 37 | 38 | -------------------------------------------------------------------------------- /Hourglass/Parsing/EmptyDateToken.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Parsing; 8 | 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Text.RegularExpressions; 12 | 13 | /// 14 | /// Represents an unspecified date. 15 | /// 16 | public sealed class EmptyDateToken : DateToken 17 | { 18 | /// 19 | public override bool IsValid => true; 20 | 21 | /// 22 | public override DateTime ToDateTime(DateTime minDate, bool inclusive) 23 | { 24 | ThrowIfNotValid(); 25 | 26 | return inclusive ? minDate.Date : minDate.Date.AddDays(1); 27 | } 28 | 29 | /// 30 | public override string ToString(IFormatProvider provider) 31 | { 32 | return string.Empty; 33 | } 34 | 35 | /// 36 | /// Parses strings. 37 | /// 38 | public new sealed class Parser : DateToken.Parser 39 | { 40 | /// 41 | /// Singleton instance of the class. 42 | /// 43 | public static readonly Parser Instance = new(); 44 | 45 | /// 46 | /// Prevents a default instance of the class from being created. 47 | /// 48 | private Parser() 49 | { 50 | } 51 | 52 | /// 53 | public override bool IsCompatibleWith(TimeToken.Parser timeTokenParser) 54 | { 55 | return timeTokenParser is not EmptyTimeToken.Parser; 56 | } 57 | 58 | /// 59 | public override IEnumerable GetPatterns(IFormatProvider provider) 60 | { 61 | yield return string.Empty; 62 | } 63 | 64 | /// 65 | protected override DateToken ParseInternal(Match match, IFormatProvider provider) 66 | { 67 | return new EmptyDateToken(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Hourglass/Managers/VirtualDesktopManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | using System.Windows.Interop; 5 | 6 | using WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | namespace Hourglass.Managers; 9 | 10 | // ReSharper disable ExceptionNotDocumented 11 | 12 | public sealed class VirtualDesktopManager : Manager 13 | { 14 | public static readonly VirtualDesktopManager Instance = new(); 15 | 16 | private readonly Lazy _currentVirtualDesktop = new(GetVirtualDesktop); 17 | 18 | private VirtualDesktopManager() 19 | { 20 | } 21 | 22 | protected override void Dispose(bool disposing) 23 | { 24 | if (Disposed) 25 | { 26 | return; 27 | } 28 | 29 | if (disposing && _currentVirtualDesktop.IsValueCreated) 30 | { 31 | _currentVirtualDesktop.Value?.Dispose(); 32 | } 33 | 34 | base.Dispose(disposing); 35 | } 36 | 37 | public void MoveToCurrentVirtualDesktop(Window window) => 38 | _currentVirtualDesktop.Value?.MoveTo(new WindowInteropHelper(window).Handle); 39 | 40 | private static ICurrentVirtualDesktop? GetVirtualDesktop() 41 | { 42 | foreach (var virtualDesktop in EnumerateVirtualDesktops()) 43 | { 44 | if (virtualDesktop.IsValid) 45 | { 46 | return virtualDesktop; 47 | } 48 | 49 | virtualDesktop.Dispose(); 50 | } 51 | 52 | return null; 53 | 54 | static IEnumerable EnumerateVirtualDesktops() 55 | { 56 | using var immersiveShellProvider = new ImmersiveShellProvider(); 57 | 58 | yield return new VirtualDesktopWin11_Insider25314(immersiveShellProvider); 59 | yield return new VirtualDesktopWin11_Insider22631(immersiveShellProvider); 60 | yield return new VirtualDesktopWin11_Insider(immersiveShellProvider); 61 | yield return new VirtualDesktopWin11_23H2_2921(immersiveShellProvider); 62 | yield return new VirtualDesktopWin11_23H2(immersiveShellProvider); 63 | yield return new VirtualDesktopWin11_22H2(immersiveShellProvider); 64 | yield return new VirtualDesktopWin11_21H2(immersiveShellProvider); 65 | yield return new VirtualDesktopWin10(immersiveShellProvider); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Update-Version.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | 4 | Gets project version from latest.xml and updates it for dependent files. 5 | 6 | .DESCRIPTION 7 | 8 | Gets project version from latest.xml and updates it for dependent files. Prints version and update URL. 9 | 10 | .EXAMPLE 11 | pwsh ./Update-Version.ps1 12 | Gets project version from latest.xml and updates it for dependent files. 13 | #> 14 | 15 | [CmdletBinding()] 16 | param() 17 | 18 | #requires -Version 7 19 | Set-StrictMode -Version Latest 20 | 21 | $ErrorActionPreference = 'Stop' 22 | $Verbose = $VerbosePreference -ne 'SilentlyContinue' 23 | 24 | $PSDefaultParameterValues['*:Verbose'] = $Verbose 25 | $PSDefaultParameterValues['*:ErrorAction'] = $ErrorActionPreference 26 | 27 | $ThisFolder = Split-Path (Get-Item (&{ $MyInvocation.ScriptName })) 28 | 29 | function Main 30 | { 31 | $latestFile = Join-Path $ThisFolder latest.xml 32 | $buildPropsFile = Join-Path $ThisFolder Directory.Build.props 33 | $appManifestFile = Join-Path $ThisFolder Hourglass\Properties\app.manifest 34 | $bundleWxsFile = Join-Path $ThisFolder Hourglass.Bundle\Bundle.wxs 35 | $productWxsFile = Join-Path $ThisFolder Hourglass.Setup\Product.wxs 36 | 37 | Write-Output "Reading '$latestFile'..." 38 | 39 | $latestXml = ([xml](Get-Content $latestFile)).UpdateInfo 40 | $latest = [PSCustomObject]@{ 41 | Version = $latestXml.LatestVersion 42 | UpdateUrl = $latestXml.UpdateUrl 43 | } 44 | 45 | $fieldsFormat = 46 | @{ n = 'Version'; e = { $_.Version } }, 47 | @{ n = 'UpdateUrl'; e = { $_.UpdateUrl } } 48 | 49 | Write-Output "`n$(($latest | Format-List $fieldsFormat | Out-String).Trim())`n" 50 | 51 | Update-Content $appManifestFile '(?<=assemblyIdentity\s+version=")[^"]+(?=[\s\S]+?name)',$latest.Version 52 | Update-Content $buildPropsFile '(?<=\)[^<]+',$latest.Version 53 | Update-Content $bundleWxsFile '(?<=\s+Version=")[^"]+',$latest.Version 54 | Update-Content $productWxsFile '(?<=\s+Version=")[^"]+',$latest.Version 55 | } 56 | 57 | function Update-Content($file, $replace) 58 | { 59 | Write-Output "Updating '$file'..." 60 | 61 | $reader = [IO.StreamReader]::new($file, [Text.Encoding]::Default, $true) 62 | [void]$reader.Peek() 63 | $encoding = $reader.CurrentEncoding 64 | $reader.Close() 65 | 66 | (Get-Content $file -Raw) -creplace $replace | Set-Content $file -NoNewline -Encoding $encoding 67 | } 68 | 69 | . Main 70 | -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/WindowSubclassHandlerNativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable all 5 | 6 | namespace KPreisser.UI; 7 | 8 | internal static class WindowSubclassHandlerNativeMethods 9 | { 10 | public const int GWLP_WNDPROC = -4; 11 | 12 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 13 | public delegate IntPtr WindowProc( 14 | IntPtr hWnd, 15 | int msg, 16 | IntPtr wParam, 17 | IntPtr lParam); 18 | 19 | [DllImport("kernel32", 20 | EntryPoint = "SetLastError", 21 | ExactSpelling = true)] 22 | public static extern void SetLastError(int dwErrCode); 23 | 24 | [DllImport("user32", 25 | EntryPoint = "CallWindowProcW", 26 | ExactSpelling = true)] 27 | public static extern IntPtr CallWindowProc( 28 | IntPtr lpPrevWndFunc, 29 | IntPtr hWnd, 30 | int msg, 31 | IntPtr wParam, 32 | IntPtr lParam); 33 | 34 | public static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) 35 | { 36 | if (IntPtr.Size == 4) 37 | return (IntPtr)GetWindowLong32(hWnd, nIndex); 38 | 39 | return GetWindowLongPtr64(hWnd, nIndex); 40 | } 41 | 42 | public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) 43 | { 44 | if (IntPtr.Size == 4) 45 | return (IntPtr)SetWindowLong32(hWnd, nIndex, (int)dwNewLong); 46 | 47 | return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); 48 | } 49 | 50 | [DllImport("user32", 51 | EntryPoint = "GetWindowLongW", 52 | ExactSpelling = true, 53 | SetLastError = true)] 54 | private static extern int GetWindowLong32(IntPtr hWnd, int nIndex); 55 | 56 | [DllImport("user32", 57 | EntryPoint = "GetWindowLongPtrW", 58 | ExactSpelling = true, 59 | SetLastError = true)] 60 | private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); 61 | 62 | [DllImport("user32", 63 | EntryPoint = "SetWindowLongW", 64 | ExactSpelling = true, 65 | SetLastError = true)] 66 | private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong); 67 | 68 | [DllImport("user32", 69 | EntryPoint = "SetWindowLongPtrW", 70 | ExactSpelling = true, 71 | SetLastError = true)] 72 | private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); 73 | } -------------------------------------------------------------------------------- /Hourglass/Managers/AppManager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using System.Linq; 10 | 11 | /// 12 | /// Manages the app. 13 | /// 14 | public sealed class AppManager : Manager 15 | { 16 | /// 17 | /// Singleton instance of the class. 18 | /// 19 | public static readonly AppManager Instance = new(); 20 | 21 | /// 22 | /// The manager class singleton instances. 23 | /// 24 | private static readonly Manager[] Managers = 25 | [ 26 | VirtualDesktopManager.Instance, 27 | TaskDialogManager.Instance, 28 | ErrorManager.Instance, 29 | SettingsManager.Instance, 30 | UpdateManager.Instance, 31 | KeepAwakeManager.Instance, 32 | WakeUpManager.Instance, 33 | NotificationAreaIconManager.Instance, 34 | ThemeManager.Instance, 35 | SoundManager.Instance, 36 | TimerStartManager.Instance, 37 | TimerOptionsManager.Instance, 38 | TimerManager.Instance 39 | ]; 40 | 41 | /// 42 | /// Prevents a default instance of the class from being created. 43 | /// 44 | private AppManager() 45 | { 46 | } 47 | 48 | /// 49 | public override void Initialize() 50 | { 51 | foreach (Manager manager in Managers) 52 | { 53 | manager.Initialize(); 54 | } 55 | } 56 | 57 | /// 58 | public override void Persist() 59 | { 60 | foreach (Manager manager in Managers.Reverse()) 61 | { 62 | manager.Persist(); 63 | } 64 | } 65 | 66 | /// 67 | protected override void Dispose(bool disposing) 68 | { 69 | if (Disposed) 70 | { 71 | return; 72 | } 73 | 74 | if (disposing) 75 | { 76 | foreach (Manager manager in Managers.Reverse()) 77 | { 78 | manager.Dispose(); 79 | } 80 | } 81 | 82 | base.Dispose(disposing); 83 | } 84 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_21H2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_21H2(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop(IntPtr.Zero).GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("536D3495-B208-4CC9-AE26-DE8111275BF8")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | IntPtr Unknown1(); 24 | 25 | [return: MarshalAs(UnmanagedType.HString)] 26 | string GetName(); 27 | 28 | [return: MarshalAs(UnmanagedType.HString)] 29 | string GetWallpaperPath(); 30 | } 31 | 32 | [ComImport] 33 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 34 | [Guid("B2F925B9-5A0F-4D2E-9F4D-2B1507593C10")] 35 | internal interface IVirtualDesktopManagerInternal 36 | { 37 | int GetCount(IntPtr hWnd); 38 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 39 | bool CanViewMoveDesktops(IntPtr view); 40 | IVirtualDesktop GetCurrentDesktop(IntPtr hWnd); 41 | void GetDesktops(IntPtr hWnd, out IntPtr desktops); 42 | 43 | [PreserveSig] 44 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 45 | 46 | void SwitchDesktop(IntPtr hWnd, IVirtualDesktop desktop); 47 | IVirtualDesktop CreateDesktop(IntPtr hWnd); 48 | void MoveDesktop(IVirtualDesktop desktop, IntPtr hWnd, int nIndex); 49 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 50 | IVirtualDesktop FindDesktop(ref Guid desktopId); 51 | void Unknown1(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 52 | void SetName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 53 | void SetWallpaperPath(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 54 | void SetAllWallpaperPaths([MarshalAs(UnmanagedType.HString)] string path); 55 | void Unknown2(IntPtr pView0, IntPtr pView1); 56 | int Unknown3(); 57 | void RemoveAll(bool remove); 58 | } 59 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogIcon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | // ReSharper disable ExceptionNotDocumented 5 | 6 | // ReSharper disable all 7 | 8 | namespace KPreisser.UI; 9 | 10 | /// 11 | /// 12 | /// 13 | public abstract class TaskDialogIcon 14 | { 15 | private static readonly IReadOnlyDictionary s_standardIcons 16 | = new Dictionary() { 17 | { TaskDialogStandardIcon.None, new(TaskDialogStandardIcon.None) }, 18 | { TaskDialogStandardIcon.Information, new(TaskDialogStandardIcon.Information) }, 19 | { TaskDialogStandardIcon.Warning, new(TaskDialogStandardIcon.Warning) }, 20 | { TaskDialogStandardIcon.Error, new(TaskDialogStandardIcon.Error) }, 21 | { TaskDialogStandardIcon.SecurityShield, new(TaskDialogStandardIcon.SecurityShield) }, 22 | { TaskDialogStandardIcon.SecurityShieldBlueBar, new(TaskDialogStandardIcon.SecurityShieldBlueBar) }, 23 | { TaskDialogStandardIcon.SecurityShieldGrayBar, new(TaskDialogStandardIcon.SecurityShieldGrayBar) }, 24 | { TaskDialogStandardIcon.SecurityWarningYellowBar, new(TaskDialogStandardIcon.SecurityWarningYellowBar) }, 25 | { TaskDialogStandardIcon.SecurityErrorRedBar, new(TaskDialogStandardIcon.SecurityErrorRedBar) }, 26 | { TaskDialogStandardIcon.SecuritySuccessGreenBar, new(TaskDialogStandardIcon.SecuritySuccessGreenBar) }, 27 | }; 28 | 29 | private protected TaskDialogIcon() 30 | { 31 | } 32 | 33 | /// 34 | /// 35 | /// 36 | /// 37 | public static implicit operator TaskDialogIcon(TaskDialogStandardIcon icon) => 38 | s_standardIcons.TryGetValue(icon, out TaskDialogStandardIconContainer result) 39 | ? result 40 | : throw new InvalidCastException(); 41 | 42 | #if !NET_STANDARD 43 | /// 44 | /// 45 | /// 46 | /// 47 | public static implicit operator TaskDialogIcon(Icon icon) 48 | { 49 | return new TaskDialogIconHandle(icon); 50 | } 51 | #endif 52 | 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | public static TaskDialogIcon Get(TaskDialogStandardIcon icon) 59 | { 60 | if (!s_standardIcons.TryGetValue(icon, out TaskDialogStandardIconContainer result)) 61 | throw new ArgumentOutOfRangeException(nameof(icon)); 62 | 63 | return result; 64 | } 65 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/DependencyObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Windows; 13 | using System.Windows.Media; 14 | 15 | /// 16 | /// Provides extensions methods for the class. 17 | /// 18 | public static class DependencyObjectExtensions 19 | { 20 | /// A . 21 | extension(DependencyObject parent) 22 | { 23 | /// 24 | /// Returns the first visual child of a that matches the specified predicate. 25 | /// 26 | /// A predicate. 27 | /// The first visual child of a that matches the specified predicate. 28 | public DependencyObject? FindVisualChild(Func predicate) 29 | { 30 | return GetAllVisualChildren(parent).FirstOrDefault(predicate!); 31 | } 32 | 33 | /// 34 | /// Returns all the visual children of a . 35 | /// 36 | /// All the visual children of a . 37 | public IEnumerable GetAllVisualChildren() 38 | { 39 | foreach (DependencyObject child in parent.GetVisualChildren()) 40 | { 41 | yield return child; 42 | 43 | foreach (DependencyObject? childOfChild in GetAllVisualChildren(child)) 44 | { 45 | yield return childOfChild; 46 | } 47 | } 48 | } 49 | 50 | /// 51 | /// Returns the immediate visual children of a . 52 | /// 53 | /// The immediate visual children of a . 54 | public IEnumerable GetVisualChildren() 55 | { 56 | int childrenCount = VisualTreeHelper.GetChildrenCount(parent); 57 | for (int i = 0; i < childrenCount; i++) 58 | { 59 | yield return VisualTreeHelper.GetChild(parent, i); 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_22H2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_22H2(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop(IntPtr.Zero).GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("536D3495-B208-4CC9-AE26-DE8111275BF8")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | IntPtr Unknown1(); 24 | [return: MarshalAs(UnmanagedType.HString)] 25 | string GetName(); 26 | [return: MarshalAs(UnmanagedType.HString)] 27 | string GetWallpaperPath(); 28 | } 29 | 30 | [ComImport] 31 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 32 | [Guid("B2F925B9-5A0F-4D2E-9F4D-2B1507593C10")] 33 | internal interface IVirtualDesktopManagerInternal 34 | { 35 | int GetCount(IntPtr hWndOrMon); 36 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 37 | bool CanViewMoveDesktops(IntPtr view); 38 | IVirtualDesktop GetCurrentDesktop(IntPtr hWndOrMon); 39 | IntPtr GetAllCurrentDesktops(); 40 | void GetDesktops(IntPtr hWndOrMon, out IntPtr desktops); 41 | [PreserveSig] 42 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 43 | void SwitchDesktop(IntPtr hWndOrMon, IVirtualDesktop desktop); 44 | IVirtualDesktop CreateDesktop(IntPtr hWndOrMon); 45 | void MoveDesktop(IVirtualDesktop desktop, IntPtr hWndOrMon, int nIndex); 46 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 47 | IVirtualDesktop FindDesktop(ref Guid desktopId); 48 | void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 49 | void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 50 | void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 51 | void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); 52 | void CopyDesktopState(IntPtr pView0, IntPtr pView1); 53 | int GetDesktopIsPerMonitor(); 54 | void SetDesktopIsPerMonitor(bool state); 55 | } 56 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_Insider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_Insider(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop(IntPtr.Zero).GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("536D3495-B208-4CC9-AE26-DE8111275BF8")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | IntPtr Unknown1(); 24 | [return: MarshalAs(UnmanagedType.HString)] 25 | string GetName(); 26 | [return: MarshalAs(UnmanagedType.HString)] 27 | string GetWallpaperPath(); 28 | } 29 | 30 | [ComImport] 31 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 32 | [Guid("88846798-1611-4D18-946B-4A67BFF58C1B")] 33 | internal interface IVirtualDesktopManagerInternal 34 | { 35 | int GetCount(IntPtr hWndOrMon); 36 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 37 | bool CanViewMoveDesktops(IntPtr view); 38 | IVirtualDesktop GetCurrentDesktop(IntPtr hWndOrMon); 39 | IntPtr GetAllCurrentDesktops(); 40 | void GetDesktops(IntPtr hWndOrMon, out IntPtr desktops); 41 | [PreserveSig] 42 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 43 | void SwitchDesktop(IntPtr hWndOrMon, IVirtualDesktop desktop); 44 | IVirtualDesktop CreateDesktop(IntPtr hWndOrMon); 45 | void MoveDesktop(IVirtualDesktop desktop, IntPtr hWndOrMon, int nIndex); 46 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 47 | IVirtualDesktop FindDesktop(ref Guid desktopId); 48 | void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 49 | void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 50 | void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 51 | void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); 52 | void CopyDesktopState(IntPtr pView0, IntPtr pView1); 53 | int GetDesktopIsPerMonitor(); 54 | void SetDesktopIsPerMonitor(bool state); 55 | } 56 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_23H2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_23H2(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop().GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | string GetName(); 24 | [return: MarshalAs(UnmanagedType.HString)] 25 | string GetWallpaperPath(); 26 | bool IsRemote(); 27 | } 28 | 29 | [ComImport] 30 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 31 | [Guid("A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E")] 32 | internal interface IVirtualDesktopManagerInternal 33 | { 34 | int GetCount(); 35 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 36 | bool CanViewMoveDesktops(IntPtr view); 37 | IVirtualDesktop GetCurrentDesktop(); 38 | void GetDesktops(out IntPtr desktops); 39 | [PreserveSig] 40 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 41 | void SwitchDesktop(IVirtualDesktop desktop); 42 | IVirtualDesktop CreateDesktop(IntPtr hWndOrMon); 43 | void MoveDesktop(IVirtualDesktop desktop, IntPtr hWndOrMon, int nIndex); 44 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 45 | IVirtualDesktop FindDesktop(ref Guid desktopId); 46 | void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 47 | void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 48 | void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 49 | void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); 50 | void CopyDesktopState(IntPtr pView0, IntPtr pView1); 51 | void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); 52 | void SwitchRemoteDesktop(IVirtualDesktop desktop); 53 | void SwitchDesktopWithAnimation(IVirtualDesktop desktop); 54 | void GetLastActiveDesktop(out IVirtualDesktop desktop); 55 | void WaitForAnimationToComplete(); 56 | } 57 | } -------------------------------------------------------------------------------- /Hourglass/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | False 15 | 16 | 17 | False 18 | 19 | 20 | 00000000-0000-0000-0000-000000000000 21 | 22 | 23 | True 24 | 25 | 26 | False 27 | 28 | 29 | False 30 | 31 | 32 | True 33 | 34 | 35 | False 36 | 37 | 38 | True 39 | 40 | 41 | True 42 | 43 | 44 | True 45 | 46 | 47 | False 48 | 49 | 50 | True 51 | 52 | 53 | False 54 | 55 | 56 | False 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_23H2_2921.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_23H2_2921(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop().GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | string GetName(); 24 | [return: MarshalAs(UnmanagedType.HString)] 25 | string GetWallpaperPath(); 26 | bool IsRemote(); 27 | } 28 | 29 | [ComImport] 30 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 31 | [Guid("53F5CA0B-158F-4124-900C-057158060B27")] 32 | internal interface IVirtualDesktopManagerInternal 33 | { 34 | int GetCount(); 35 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 36 | bool CanViewMoveDesktops(IntPtr view); 37 | IVirtualDesktop GetCurrentDesktop(); 38 | void GetDesktops(out IntPtr desktops); 39 | [PreserveSig] 40 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 41 | void SwitchDesktop(IVirtualDesktop desktop); 42 | IVirtualDesktop CreateDesktop(IntPtr hWndOrMon); 43 | void MoveDesktop(IVirtualDesktop desktop, IntPtr hWndOrMon, int nIndex); 44 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 45 | IVirtualDesktop FindDesktop(ref Guid desktopId); 46 | void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 47 | void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 48 | void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 49 | void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); 50 | void CopyDesktopState(IntPtr pView0, IntPtr pView1); 51 | void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); 52 | void SwitchRemoteDesktop(IVirtualDesktop desktop, Enum VirtualDesktopSwitchType); 53 | void SwitchDesktopWithAnimation(IVirtualDesktop desktop); 54 | void GetLastActiveDesktop(out IVirtualDesktop desktop); 55 | void WaitForAnimationToComplete(); 56 | } 57 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_Insider22631.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_Insider22631(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop().GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | [return: MarshalAs(UnmanagedType.HString)] 24 | string GetName(); 25 | [return: MarshalAs(UnmanagedType.HString)] 26 | string GetWallpaperPath(); 27 | bool IsRemote(); 28 | } 29 | 30 | [ComImport] 31 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 32 | [Guid("53F5CA0B-158F-4124-900C-057158060B27")] 33 | internal interface IVirtualDesktopManagerInternal 34 | { 35 | int GetCount(); 36 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 37 | bool CanViewMoveDesktops(IntPtr view); 38 | IVirtualDesktop GetCurrentDesktop(); 39 | void GetDesktops(out IntPtr desktops); 40 | [PreserveSig] 41 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 42 | void SwitchDesktop(IVirtualDesktop desktop); 43 | void SwitchDesktopAndMoveForegroundView(IVirtualDesktop desktop); 44 | IVirtualDesktop CreateDesktop(); 45 | void MoveDesktop(IVirtualDesktop desktop, int nIndex); 46 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 47 | IVirtualDesktop FindDesktop(ref Guid desktopId); 48 | void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 49 | void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 50 | void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 51 | void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); 52 | void CopyDesktopState(IntPtr pView0, IntPtr pView1); 53 | void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); 54 | void SwitchRemoteDesktop(IVirtualDesktop desktop); 55 | void SwitchDesktopWithAnimation(IVirtualDesktop desktop); 56 | void GetLastActiveDesktop(out IVirtualDesktop desktop); 57 | void WaitForAnimationToComplete(); 58 | } 59 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/Implementation/VirtualDesktopWin11_Insider25314.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | #pragma warning disable S101 9 | internal sealed class VirtualDesktopWin11_Insider25314(ImmersiveShellProvider immersiveShellProvider) 10 | #pragma warning restore S101 11 | : VirtualDesktop(immersiveShellProvider) 12 | { 13 | protected override Guid GetCurrentDesktopId() => 14 | VirtualDesktopManagerInternal!.GetCurrentDesktop().GetId(); 15 | 16 | [ComImport] 17 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 18 | [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] 19 | internal interface IVirtualDesktop 20 | { 21 | bool IsViewVisible(IntPtr view); 22 | Guid GetId(); 23 | [return: MarshalAs(UnmanagedType.HString)] 24 | string GetName(); 25 | [return: MarshalAs(UnmanagedType.HString)] 26 | string GetWallpaperPath(); 27 | bool IsRemote(); 28 | } 29 | 30 | [ComImport] 31 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 32 | [Guid("A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E")] 33 | internal interface IVirtualDesktopManagerInternal 34 | { 35 | int GetCount(); 36 | void MoveViewToDesktop(IntPtr view, IVirtualDesktop desktop); 37 | bool CanViewMoveDesktops(IntPtr view); 38 | IVirtualDesktop GetCurrentDesktop(); 39 | void GetDesktops(out IntPtr desktops); 40 | [PreserveSig] 41 | int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); 42 | void SwitchDesktop(IVirtualDesktop desktop); 43 | void SwitchDesktopAndMoveForegroundView(IVirtualDesktop desktop); 44 | IVirtualDesktop CreateDesktop(); 45 | void MoveDesktop(IVirtualDesktop desktop, int nIndex); 46 | void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); 47 | IVirtualDesktop FindDesktop(ref Guid desktopId); 48 | void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IntPtr unknown1, out IntPtr unknown2); 49 | void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); 50 | void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); 51 | void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); 52 | void CopyDesktopState(IntPtr pView0, IntPtr pView1); 53 | void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); 54 | void SwitchRemoteDesktop(IVirtualDesktop desktop); 55 | void SwitchDesktopWithAnimation(IVirtualDesktop desktop); 56 | void GetLastActiveDesktop(out IVirtualDesktop desktop); 57 | void WaitForAnimationToComplete(); 58 | } 59 | } -------------------------------------------------------------------------------- /Hourglass/Lib/WindowsVirtualDesktopHelper/Source/VirtualDesktopAPI/VirtualDesktop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | namespace WindowsVirtualDesktopHelper.VirtualDesktopAPI.Implementation; 7 | 8 | internal abstract class VirtualDesktop : ICurrentVirtualDesktop 9 | where TVirtualDesktopManagerInternal : class 10 | { 11 | private IVirtualDesktopManager? _virtualDesktopManager; 12 | 13 | protected TVirtualDesktopManagerInternal? VirtualDesktopManagerInternal; 14 | 15 | protected VirtualDesktop(ImmersiveShellProvider immersiveShellProvider) 16 | { 17 | try 18 | { 19 | VirtualDesktopManagerInternal = immersiveShellProvider.QueryService(new("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B" /* CLSID_VirtualDesktopManagerInternal */)); 20 | 21 | if (VirtualDesktopManagerInternal is not null) 22 | { 23 | _virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(new("AA509086-5CA9-4C25-8F95-589D3C07B48A" /* CLSID_VirtualDesktopManager */))); 24 | } 25 | } 26 | catch 27 | { 28 | // Ignore. 29 | } 30 | } 31 | 32 | protected abstract Guid GetCurrentDesktopId(); 33 | 34 | public bool IsValid => 35 | _virtualDesktopManager is not null && 36 | VirtualDesktopManagerInternal is not null; 37 | 38 | public void MoveTo(IntPtr handle) 39 | { 40 | try 41 | { 42 | if (!IsValid) 43 | { 44 | return; 45 | } 46 | 47 | if (_virtualDesktopManager!.IsWindowOnCurrentVirtualDesktop(handle)) 48 | { 49 | return; 50 | } 51 | 52 | _virtualDesktopManager!.MoveWindowToDesktop(handle, GetCurrentDesktopId()); 53 | } 54 | catch 55 | { 56 | // Ignore. 57 | } 58 | } 59 | 60 | public void Dispose() 61 | { 62 | ReleaseComObject(_virtualDesktopManager); 63 | ReleaseComObject(VirtualDesktopManagerInternal); 64 | 65 | _virtualDesktopManager = null; 66 | VirtualDesktopManagerInternal = null; 67 | 68 | static void ReleaseComObject(object? o) 69 | { 70 | if (o is not null) 71 | { 72 | Marshal.FinalReleaseComObject(o); 73 | } 74 | } 75 | } 76 | } 77 | 78 | internal interface ICurrentVirtualDesktop: IDisposable 79 | { 80 | bool IsValid { get; } 81 | 82 | void MoveTo(IntPtr handle); 83 | } 84 | 85 | [ComImport] 86 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 87 | [Guid("A5CD92FF-29BE-454C-8D04-D82879FB3F1B")] 88 | internal interface IVirtualDesktopManager 89 | { 90 | bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow); 91 | Guid GetWindowDesktopId(IntPtr topLevelWindow); 92 | void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId); 93 | } 94 | -------------------------------------------------------------------------------- /Hourglass/Managers/TimerOptionsManager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using System.Linq; 10 | using System.Windows; 11 | 12 | using Properties; 13 | using Timing; 14 | using Windows; 15 | 16 | /// 17 | /// Manages s. 18 | /// 19 | public sealed class TimerOptionsManager : Manager 20 | { 21 | /// 22 | /// Singleton instance of the class. 23 | /// 24 | public static readonly TimerOptionsManager Instance = new(); 25 | 26 | /// 27 | /// The most recent . 28 | /// 29 | private TimerOptions _mostRecentOptions = new(); 30 | 31 | /// 32 | /// Prevents a default instance of the class from being created. 33 | /// 34 | private TimerOptionsManager() 35 | { 36 | } 37 | 38 | /// 39 | /// Gets the most recent . 40 | /// 41 | public TimerOptions MostRecentOptions 42 | { 43 | get 44 | { 45 | UpdateMostRecentOptions(); 46 | return TimerOptions.FromTimerOptions(_mostRecentOptions)!; 47 | } 48 | } 49 | 50 | /// 51 | public override void Initialize() 52 | { 53 | _mostRecentOptions = Settings.Default.MostRecentOptions; 54 | } 55 | 56 | /// 57 | public override void Persist() 58 | { 59 | UpdateMostRecentOptions(); 60 | Settings.Default.MostRecentOptions = _mostRecentOptions; 61 | } 62 | 63 | /// 64 | /// Updates the from the currently opened s. 65 | /// 66 | private void UpdateMostRecentOptions() 67 | { 68 | if (Application.Current is null) 69 | { 70 | return; 71 | } 72 | 73 | // Get the options most recently shown to the user from a window that is still open 74 | var options = Application.Current.Windows.OfType() 75 | .Where(static window => window.IsVisible) 76 | .OrderByDescending(static window => window.MenuLastShown) 77 | .Select(static window => window.Options); 78 | 79 | _mostRecentOptions = TimerOptions.FromTimerOptions(options.FirstOrDefault()) ?? _mostRecentOptions; 80 | 81 | // Never save a title 82 | _mostRecentOptions.Title = string.Empty; 83 | 84 | // Never save shutting down when expired or lock interface options 85 | _mostRecentOptions.ShutDownWhenExpired = false; 86 | _mostRecentOptions.LockInterface = false; 87 | } 88 | } -------------------------------------------------------------------------------- /Hourglass.Setup/Product.wxs: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Hourglass/Extensions/ResourceManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Globalization; 11 | using System.Resources; 12 | 13 | // ReSharper disable ExceptionNotDocumented 14 | 15 | /// 16 | /// Provides extensions methods for the class. 17 | /// 18 | public static class ResourceManagerExtensions 19 | { 20 | /// A . 21 | extension(ResourceManager resourceManager) 22 | { 23 | /// 24 | /// Returns an for the culture that is actually loaded when retrieving resources. 25 | /// 26 | /// An for the culture that is actually loaded when retrieving 27 | /// resources. 28 | public IFormatProvider GetEffectiveProvider() 29 | { 30 | string? cultureName = resourceManager.GetString(nameof(Properties.Resources.CultureName)); 31 | return !string.IsNullOrWhiteSpace(cultureName) ? CultureInfo.GetCultureInfo(cultureName) : CultureInfo.InvariantCulture; 32 | } 33 | 34 | /// 35 | /// Returns an for the culture that is actually loaded when retrieving resources 36 | /// for the culture specified by . 37 | /// 38 | /// An that is a . 39 | /// An for the culture that is actually loaded when retrieving resources 40 | /// for the culture specified by . 41 | public IFormatProvider GetEffectiveProvider(IFormatProvider provider) 42 | { 43 | string? cultureName = resourceManager.GetString(nameof(Properties.Resources.CultureName), (CultureInfo)provider); 44 | return !string.IsNullOrWhiteSpace(cultureName) ? CultureInfo.GetCultureInfo(cultureName) : CultureInfo.InvariantCulture; 45 | } 46 | 47 | /// 48 | /// Returns the value of the string resource localized for a culture specified by an . 49 | /// 50 | /// The name of the resource to retrieve. 51 | /// An that is a . 52 | /// The value of the resource localized for the specified culture, or null if cannot be found in a resource set. 54 | public string GetString(string name, IFormatProvider provider) 55 | { 56 | return resourceManager.GetString(name, (CultureInfo)provider)!; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Hourglass/Serialization/ThemeInfo.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Serialization; 8 | 9 | using System.Windows.Media; 10 | 11 | using Timing; 12 | 13 | /// 14 | /// The representation of a used for XML serialization. 15 | /// 16 | public sealed class ThemeInfo 17 | { 18 | /// 19 | /// Gets or sets the unique identifier for this theme. 20 | /// 21 | public string Identifier { get; set; } = null!; 22 | 23 | /// 24 | /// Gets or sets the friendly name for this theme, or null if no friendly name is specified. 25 | /// 26 | public string Name { get; set; } = null!; 27 | 28 | /// 29 | /// Gets or sets the background color of the window. 30 | /// 31 | public Color BackgroundColor { get; set; } 32 | 33 | /// 34 | /// Gets or sets the color of the progress bar. 35 | /// 36 | public Color ProgressBarColor { get; set; } 37 | 38 | /// 39 | /// Gets or sets the background color of the progress bar. 40 | /// 41 | public Color ProgressBackgroundColor { get; set; } 42 | 43 | /// 44 | /// Gets or sets the color that is flashed on expiration. 45 | /// 46 | public Color ExpirationFlashColor { get; set; } 47 | 48 | /// 49 | /// Gets or sets the color of the primary text. 50 | /// 51 | public Color PrimaryTextColor { get; set; } 52 | 53 | /// 54 | /// Gets or sets the color of the watermark in the primary text box. 55 | /// 56 | public Color PrimaryHintColor { get; set; } 57 | 58 | /// 59 | /// Gets or sets the color of any secondary text. 60 | /// 61 | public Color SecondaryTextColor { get; set; } 62 | 63 | /// 64 | /// Gets or sets the color of the watermark in any secondary text box. 65 | /// 66 | public Color SecondaryHintColor { get; set; } 67 | 68 | /// 69 | /// Gets or sets the color of the button text. 70 | /// 71 | public Color ButtonColor { get; set; } 72 | 73 | /// 74 | /// Gets or sets the color of the button text when the user hovers over the button. 75 | /// 76 | public Color ButtonHoverColor { get; set; } 77 | 78 | /// 79 | /// Gets or sets whether the user theme is the dark one. 80 | /// 81 | public bool IsUserThemeDark { get; set; } 82 | 83 | /// 84 | /// Returns a for the specified . 85 | /// 86 | /// A . 87 | /// A for the specified . 88 | public static ThemeInfo? FromTheme(Theme? options) 89 | { 90 | return options?.ToThemeInfo(); 91 | } 92 | } -------------------------------------------------------------------------------- /Hourglass/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | False 10 | 11 | 12 | False 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 00000000-0000-0000-0000-000000000000 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | True 31 | 32 | 33 | False 34 | 35 | 36 | False 37 | 38 | 39 | True 40 | 41 | 42 | False 43 | 44 | 45 | True 46 | 47 | 48 | True 49 | 50 | 51 | True 52 | 53 | 54 | False 55 | 56 | 57 | True 58 | 59 | 60 | False 61 | 62 | 63 | False 64 | 65 | 66 | -------------------------------------------------------------------------------- /Hourglass/Managers/TimerStartManager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | 12 | using Properties; 13 | using Timing; 14 | 15 | /// 16 | /// Manages recent objects. 17 | /// 18 | public sealed class TimerStartManager : Manager 19 | { 20 | /// 21 | /// The maximum number of s. 22 | /// 23 | public const int Capacity = 10; 24 | 25 | /// 26 | /// Singleton instance of the class. 27 | /// 28 | public static readonly TimerStartManager Instance = new(); 29 | 30 | /// 31 | /// The most recent objects in reverse chronological order. 32 | /// 33 | private readonly List _timerStarts = new(Capacity); 34 | 35 | /// 36 | /// Prevents a default instance of the class from being created. 37 | /// 38 | private TimerStartManager() 39 | { 40 | } 41 | 42 | /// 43 | /// Gets a list of the most recent objects in reverse chronological order. 44 | /// 45 | #pragma warning disable S2365 46 | public IReadOnlyCollection TimerStarts => _timerStarts.Where(static e => e.IsCurrent).ToArray(); 47 | #pragma warning restore S2365 48 | 49 | /// 50 | /// Gets the most recent , or the default if there are no objects in . 51 | /// 52 | public TimerStart LastTimerStart => _timerStarts.Find(static e => e.IsCurrent) ?? TimerStart.Default; 53 | 54 | /// 55 | public override void Initialize() 56 | { 57 | _timerStarts.Clear(); 58 | _timerStarts.AddRange(Settings.Default.TimerStarts); 59 | } 60 | 61 | /// 62 | public override void Persist() 63 | { 64 | Settings.Default.TimerStarts = _timerStarts; 65 | } 66 | 67 | /// 68 | /// Adds a to the list of recent objects. 69 | /// 70 | /// A . 71 | public void Add(TimerStart timerStart) 72 | { 73 | // Remove all equivalent objects 74 | _timerStarts.RemoveAll(e => e.ToString() == timerStart.ToString()); 75 | 76 | // Add the object to the top of the list 77 | _timerStarts.Insert(0, timerStart); 78 | 79 | // Limit the number of objects in the list 80 | while (_timerStarts.Count > Capacity) 81 | { 82 | _timerStarts.RemoveAt(_timerStarts.Count - 1); 83 | } 84 | } 85 | 86 | /// 87 | /// Clears the list of recent objects. 88 | /// 89 | public void Clear() 90 | { 91 | _timerStarts.Clear(); 92 | } 93 | } -------------------------------------------------------------------------------- /Hourglass/Windows/ErrorDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Windows; 8 | 9 | using System.Windows; 10 | 11 | using Extensions; 12 | 13 | // ReSharper disable MismatchedFileName 14 | 15 | /// 16 | /// A window that displays an error. 17 | /// 18 | public sealed partial class ErrorDialog 19 | { 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | public ErrorDialog() 24 | { 25 | InitializeComponent(); 26 | InitializeResources(); 27 | InitializeMaxSize(); 28 | } 29 | 30 | /// 31 | /// Opens the window and returns only when the window is closed. 32 | /// 33 | /// The title for the error dialog.. 34 | /// The error message to show. (Optional.) 35 | /// Details of the error, such as a call stack. (Optional.) 36 | public void ShowDialog(string title, string? message = null, string? details = null) 37 | { 38 | Application.Current.ClearJumpList(); 39 | 40 | TitleTextBlock.Text = title; 41 | 42 | MessageTextBox.Text = message ?? string.Empty; 43 | MessageBorder.Visibility = string.IsNullOrWhiteSpace(message).ToVisibilityReversed(); 44 | 45 | DetailsTextBox.Text = details ?? string.Empty; 46 | ShowDetailsButton.IsEnabled = !string.IsNullOrWhiteSpace(details); 47 | 48 | ShowDialog(); 49 | } 50 | 51 | /// 52 | /// Initializes localized resources. 53 | /// 54 | private void InitializeResources() 55 | { 56 | Title = Properties.Resources.ErrorDialogTitle; 57 | TitleTextBlock.Text = Properties.Resources.ErrorDialogDefaultMessageText; 58 | ShowDetailsButton.Content = Properties.Resources.ErrorDialogShowDetailsButtonContent; 59 | CloseButton.Content = Properties.Resources.ErrorDialogCloseButtonContent; 60 | } 61 | 62 | /// 63 | /// Initializes the and properties. 64 | /// 65 | private void InitializeMaxSize() 66 | { 67 | MaxWidth = 0.75 * SystemParameters.WorkArea.Width; 68 | 69 | double maxHeight = 0.75 * SystemParameters.WorkArea.Height; 70 | 71 | TitleTextBlock.MaxHeight = maxHeight * 0.1; 72 | MessageTextBox.MaxHeight = maxHeight * 0.15; 73 | DetailsTextBox.MaxHeight = maxHeight * 0.75; 74 | } 75 | 76 | /// 77 | /// Invoked when the is clicked. 78 | /// 79 | /// The . 80 | /// The event data. 81 | private void ShowDetailsButtonClick(object sender, RoutedEventArgs e) 82 | { 83 | if (DetailsBorder.Visibility != Visibility.Visible) 84 | { 85 | DetailsBorder.Visibility = Visibility.Visible; 86 | ShowDetailsButton.IsEnabled = false; 87 | CloseButton.Focus(); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogCheckbox.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | 5 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 6 | 7 | // ReSharper disable all 8 | 9 | namespace KPreisser.UI; 10 | 11 | /// 12 | /// 13 | /// 14 | public sealed class TaskDialogCheckBox : TaskDialogControl 15 | { 16 | private string _text; 17 | 18 | private bool _checked; 19 | 20 | /// 21 | /// 22 | /// 23 | public event EventHandler CheckedChanged; 24 | 25 | /// 26 | /// 27 | /// 28 | public TaskDialogCheckBox() 29 | { 30 | } 31 | 32 | /// 33 | /// 34 | /// 35 | /// 36 | public TaskDialogCheckBox(string text) 37 | : this() 38 | { 39 | _text = text; 40 | } 41 | 42 | /// 43 | /// 44 | /// 45 | public string Text 46 | { 47 | get => _text; 48 | 49 | set 50 | { 51 | DenyIfBound(); 52 | 53 | _text = value; 54 | } 55 | } 56 | 57 | /// 58 | /// 59 | /// 60 | /// 61 | /// This property can be set while the dialog is shown. 62 | /// 63 | public bool Checked 64 | { 65 | get => _checked; 66 | 67 | set 68 | { 69 | DenyIfBoundAndNotCreated(); 70 | DenyIfWaitingForInitialization(); 71 | 72 | if (BoundPage == null) 73 | { 74 | _checked = value; 75 | } 76 | else 77 | { 78 | // Click the checkbox which should cause a call to 79 | // HandleCheckBoxClicked(), where we will update the checked 80 | // state. 81 | BoundPage.BoundTaskDialog.ClickCheckBox( 82 | value); 83 | } 84 | } 85 | } 86 | 87 | internal override bool IsCreatable 88 | { 89 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 90 | } 91 | 92 | /// 93 | /// 94 | /// 95 | public void Focus() 96 | { 97 | DenyIfNotBoundOrWaitingForInitialization(); 98 | DenyIfBoundAndNotCreated(); 99 | 100 | BoundPage.BoundTaskDialog.ClickCheckBox( 101 | _checked, 102 | true); 103 | } 104 | 105 | /// 106 | /// 107 | /// 108 | /// 109 | public override string ToString() 110 | { 111 | return _text ?? base.ToString(); 112 | } 113 | 114 | internal void HandleCheckBoxClicked(bool @checked) 115 | { 116 | // Only raise the event if the state actually changed. 117 | if (@checked != _checked) 118 | { 119 | _checked = @checked; 120 | OnCheckedChanged(EventArgs.Empty); 121 | } 122 | } 123 | 124 | private protected override TaskDialogFlags BindCore() 125 | { 126 | TaskDialogFlags flags = base.BindCore(); 127 | 128 | if (_checked) 129 | flags |= TaskDialogFlags.TDF_VERIFICATION_FLAG_CHECKED; 130 | 131 | return flags; 132 | } 133 | 134 | /// 135 | /// 136 | /// 137 | /// 138 | private void OnCheckedChanged(EventArgs e) 139 | { 140 | CheckedChanged?.Invoke(this, e); 141 | } 142 | } -------------------------------------------------------------------------------- /Hourglass/Properties/Settings.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Properties; 8 | 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | 12 | using Serialization; 13 | using Timing; 14 | using Windows; 15 | 16 | /// 17 | /// Application settings. 18 | /// 19 | #if PORTABLE 20 | [System.Configuration.SettingsProvider(typeof(PortableSettingsProvider))] 21 | #endif 22 | internal sealed partial class Settings 23 | { 24 | /// 25 | /// Gets or sets the most recent . 26 | /// 27 | public TimerOptions MostRecentOptions 28 | { 29 | get => TimerOptions.FromTimerOptionsInfo(MostRecentOptionsInfo) ?? new(); 30 | set => MostRecentOptionsInfo = TimerOptionsInfo.FromTimerOptions(value); 31 | } 32 | 33 | /// 34 | /// Gets or sets the s. 35 | /// 36 | public IList Timers 37 | { 38 | get 39 | { 40 | IEnumerable timerInfos = TimerInfos ?? []; 41 | #pragma warning disable S2365 42 | return timerInfos.Select(Timer.FromTimerInfo).Where(static t => t is not null).ToList()!; 43 | #pragma warning restore S2365 44 | } 45 | 46 | set 47 | { 48 | IEnumerable timerInfos = value.Select(TimerInfo.FromTimer).Where(static t => t is not null)!; 49 | TimerInfos = new(timerInfos); 50 | } 51 | } 52 | 53 | /// 54 | /// Gets or sets the s. 55 | /// 56 | public IList TimerStarts 57 | { 58 | get 59 | { 60 | IEnumerable timerStartInfos = TimerStartInfos ?? []; 61 | #pragma warning disable S2365 62 | return timerStartInfos.Select(TimerStart.FromTimerStartInfo).Where(static t => t is not null).ToList()!; 63 | #pragma warning restore S2365 64 | } 65 | 66 | set 67 | { 68 | IEnumerable timerStartInfos = value.Select(TimerStartInfo.FromTimerStart)!; 69 | TimerStartInfos = new(timerStartInfos); 70 | } 71 | } 72 | 73 | /// 74 | /// Gets or sets the collection of the themes defined by the user. 75 | /// 76 | public IList UserProvidedThemes 77 | { 78 | get 79 | { 80 | IEnumerable userProvidedThemeInfos = UserProvidedThemeInfos ?? []; 81 | #pragma warning disable S2365 82 | return userProvidedThemeInfos.Select(Theme.FromThemeInfo).Where(static t => t is not null).ToList()!; 83 | #pragma warning restore S2365 84 | } 85 | 86 | set 87 | { 88 | IEnumerable userProvidedThemeInfos = value.Select(ThemeInfo.FromTheme).Where(static t => t is not null); 89 | UserProvidedThemeInfos = new(userProvidedThemeInfos!); 90 | } 91 | } 92 | 93 | /// 94 | /// Gets or sets the . 95 | /// 96 | public WindowSize? WindowSize 97 | { 98 | get => WindowSize.FromWindowSizeInfo(WindowSizeInfo); 99 | set => WindowSizeInfo = WindowSizeInfo.FromWindowSize(value); 100 | } 101 | } -------------------------------------------------------------------------------- /Hourglass/Windows/ColorControl.xaml.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Windows; 8 | 9 | using System; 10 | using System.Linq; 11 | using System.Windows; 12 | using System.Windows.Forms; 13 | using System.Windows.Media; 14 | 15 | using Extensions; 16 | using Timing; 17 | 18 | // ReSharper disable MismatchedFileName 19 | 20 | /// 21 | /// A control for displaying and selecting a . 22 | /// 23 | public sealed partial class ColorControl 24 | { 25 | /// 26 | /// A that specifies the text label. 27 | /// 28 | public static readonly DependencyProperty TextProperty 29 | = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ColorControl)); 30 | 31 | /// 32 | /// A that specifies the color. 33 | /// 34 | public static readonly DependencyProperty ColorProperty 35 | = DependencyProperty.Register(nameof(Color), typeof(Color), typeof(ColorControl)); 36 | 37 | /// 38 | /// A that specifies the theme to which the color belongs. 39 | /// 40 | public static readonly DependencyProperty ThemeProperty 41 | = DependencyProperty.Register(nameof(Theme), typeof(Theme), typeof(ColorControl)); 42 | 43 | /// 44 | /// Initializes a new instance of the class. 45 | /// 46 | public ColorControl() 47 | { 48 | InitializeComponent(); 49 | } 50 | 51 | /// 52 | /// Occurs when the property changes. 53 | /// 54 | public event EventHandler? ColorChanged; 55 | 56 | /// 57 | /// Gets or sets the text label. 58 | /// 59 | public string Text 60 | { 61 | get => (string) GetValue(TextProperty); 62 | set => SetValue(TextProperty, value); 63 | } 64 | 65 | /// 66 | /// Gets or sets the color. 67 | /// 68 | public Color Color 69 | { 70 | get => (Color) GetValue(ColorProperty); 71 | set => SetValue(ColorProperty, value); 72 | } 73 | 74 | /// 75 | /// Gets or sets the theme to which the color belongs. 76 | /// 77 | public Theme Theme 78 | { 79 | get => (Theme) GetValue(ThemeProperty); 80 | set => SetValue(ThemeProperty, value); 81 | } 82 | 83 | /// 84 | /// Invoked when the is clicked. 85 | /// 86 | /// The . 87 | /// The event data. 88 | private void ButtonClick(object sender, RoutedEventArgs e) 89 | { 90 | ColorDialog dialog = new() 91 | { 92 | AnyColor = true, 93 | FullOpen = true, 94 | CustomColors = Theme.GetPalette().Select(static c => c.ToInt()).ToArray() 95 | }; 96 | 97 | DialogResult result = dialog.ShowDialog(); 98 | if (result == DialogResult.OK) 99 | { 100 | Color = Color.FromRgb(dialog.Color.R, dialog.Color.G, dialog.Color.B); 101 | ColorChanged?.Invoke(this /* sender */, EventArgs.Empty); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogCustomButton.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | 5 | // ReSharper disable all 6 | 7 | namespace KPreisser.UI; 8 | 9 | /// 10 | /// 11 | /// 12 | public sealed class TaskDialogCustomButton : TaskDialogButton 13 | { 14 | private string _text; 15 | 16 | private string _descriptionText; 17 | 18 | private int _buttonID; 19 | 20 | /// 21 | /// 22 | /// 23 | public TaskDialogCustomButton() 24 | { 25 | } 26 | 27 | /// 28 | /// 29 | /// 30 | public TaskDialogCustomButton(string text, string descriptionText = null) 31 | : this() 32 | { 33 | _text = text; 34 | _descriptionText = descriptionText; 35 | } 36 | 37 | /// 38 | /// 39 | /// 40 | public string Text 41 | { 42 | get => _text; 43 | 44 | set 45 | { 46 | DenyIfBound(); 47 | 48 | _text = value; 49 | } 50 | } 51 | 52 | /// 53 | /// Gets or sets an additional description text that will be displayed in 54 | /// a separate line of the command link when 55 | /// is set to 56 | /// or 57 | /// . 58 | /// 59 | public string DescriptionText 60 | { 61 | get => _descriptionText; 62 | 63 | set 64 | { 65 | DenyIfBound(); 66 | 67 | _descriptionText = value; 68 | } 69 | } 70 | 71 | internal override bool IsCreatable 72 | { 73 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 74 | } 75 | 76 | internal override int ButtonID 77 | { 78 | get => _buttonID; 79 | } 80 | 81 | internal new TaskDialogCustomButtonCollection Collection 82 | { 83 | get => (TaskDialogCustomButtonCollection)base.Collection; 84 | set => base.Collection = value; 85 | } 86 | 87 | /// 88 | /// 89 | /// 90 | /// 91 | public override string ToString() 92 | { 93 | return _text ?? base.ToString(); 94 | } 95 | 96 | internal TaskDialogFlags Bind(TaskDialogPage page, int buttonID) 97 | { 98 | TaskDialogFlags result = Bind(page); 99 | _buttonID = buttonID; 100 | 101 | return result; 102 | } 103 | 104 | internal string GetResultingText() 105 | { 106 | TaskDialogPage page = BoundPage; 107 | 108 | // Remove LFs from the text. Otherwise, the dialog would display the 109 | // part of the text after the LF in the command link note, but for 110 | // this we have the "DescriptionText" property, so we should ensure that 111 | // there is not an discrepancy here and that the contents of the "Text" 112 | // property are not displayed in the command link note. 113 | // Therefore, we replace a combined CR+LF with CR, and then also single 114 | // LFs with CR, because CR is treated as a line break. 115 | string text = _text?.Replace("\r\n", "\r").Replace("\n", "\r"); 116 | 117 | if (page?.CustomButtonStyle is TaskDialogCustomButtonStyle.CommandLinks or TaskDialogCustomButtonStyle.CommandLinksNoIcon && 118 | text != null && _descriptionText != null) 119 | text += '\n' + _descriptionText; 120 | 121 | return text; 122 | } 123 | 124 | private protected override void UnbindCore() 125 | { 126 | _buttonID = 0; 127 | 128 | base.UnbindCore(); 129 | } 130 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/MathExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Runtime.CompilerServices; 11 | 12 | /// 13 | /// Provides constants and static methods for trigonometric, logarithmic, and other common mathematical functions 14 | /// that extend those provided by the class. 15 | /// 16 | public static class MathExtensions 17 | { 18 | /// 19 | /// Limits a value to a specified range. 20 | /// 21 | /// A value. 22 | /// The minimum value of the range (inclusive). 23 | /// The maximum value of the range (inclusive). 24 | /// limited to the specified range. 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static double LimitToRange(double value, double min, double max) 27 | { 28 | if (value < min) 29 | { 30 | return min; 31 | } 32 | 33 | return value > max ? max : value; 34 | } 35 | 36 | /// 37 | /// Returns the latter of two s. 38 | /// 39 | /// The first to compare. 40 | /// The second to compare. 41 | /// or , whichever is later. 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public static DateTime Max(DateTime a, DateTime b) 44 | { 45 | return a > b ? a : b; 46 | } 47 | 48 | /// 49 | /// Returns the larger of two s. 50 | /// 51 | /// The first to compare. 52 | /// The second to compare. 53 | /// or , whichever is larger. 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | public static TimeSpan Max(TimeSpan a, TimeSpan b) 56 | { 57 | return a > b ? a : b; 58 | } 59 | 60 | /// 61 | /// Returns the earlier of two s. 62 | /// 63 | /// The first to compare. 64 | /// The second to compare. 65 | /// or , whichever is earlier. 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public static DateTime Min(DateTime a, DateTime b) 68 | { 69 | return a < b ? a : b; 70 | } 71 | 72 | /// 73 | /// Returns the smaller of two s. 74 | /// 75 | /// The first to compare. 76 | /// The second to compare. 77 | /// or , whichever is smaller. 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public static TimeSpan Min(TimeSpan a, TimeSpan b) 80 | { 81 | return a < b ? a : b; 82 | } 83 | 84 | // https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Double.cs,155 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | public static bool IsFinite(this double d) 87 | { 88 | long bits = BitConverter.DoubleToInt64Bits(d); 89 | return (bits & 0x7FFFFFFFFFFFFFFF) < 0x7FF0000000000000; 90 | } 91 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogRadioButtonCollection.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | 7 | // ReSharper disable all 8 | 9 | namespace KPreisser.UI; 10 | 11 | /// 12 | /// 13 | /// 14 | public class TaskDialogRadioButtonCollection 15 | : Collection 16 | { 17 | // HashSet to detect duplicate items. 18 | private readonly HashSet _itemSet = 19 | []; 20 | 21 | /// 22 | /// 23 | /// 24 | public TaskDialogRadioButtonCollection() 25 | { 26 | } 27 | 28 | internal TaskDialogPage BoundPage { get; set; } 29 | 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | public TaskDialogRadioButton Add(string text) 36 | { 37 | var button = new TaskDialogRadioButton() 38 | { 39 | Text = text 40 | }; 41 | 42 | Add(button); 43 | return button; 44 | } 45 | 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | protected override void SetItem(int index, TaskDialogRadioButton item) 52 | { 53 | // Disallow collection modification, so that we don't need to copy it 54 | // when binding the TaskDialogPage. 55 | BoundPage?.DenyIfBound(); 56 | DenyIfHasOtherCollection(item); 57 | 58 | TaskDialogRadioButton oldItem = this[index]; 59 | if (oldItem != item) 60 | { 61 | // First, add the new item (which will throw if it is a duplicate entry), 62 | // then remove the old one. 63 | if (!_itemSet.Add(item)) 64 | throw new ArgumentException($"Item {item.Text} is already present", nameof(item)); 65 | _itemSet.Remove(oldItem); 66 | 67 | oldItem.Collection = null; 68 | item.Collection = this; 69 | } 70 | 71 | base.SetItem(index, item); 72 | } 73 | 74 | /// 75 | /// 76 | /// 77 | /// 78 | /// 79 | protected override void InsertItem(int index, TaskDialogRadioButton item) 80 | { 81 | // Disallow collection modification, so that we don't need to copy it 82 | // when binding the TaskDialogPage. 83 | BoundPage?.DenyIfBound(); 84 | DenyIfHasOtherCollection(item); 85 | 86 | if (!_itemSet.Add(item)) 87 | throw new ArgumentException($"Item {item.Text} is already present", nameof(item)); 88 | 89 | item.Collection = this; 90 | base.InsertItem(index, item); 91 | } 92 | 93 | /// 94 | /// 95 | /// 96 | /// 97 | protected override void RemoveItem(int index) 98 | { 99 | // Disallow collection modification, so that we don't need to copy it 100 | // when binding the TaskDialogPage. 101 | BoundPage?.DenyIfBound(); 102 | 103 | TaskDialogRadioButton oldItem = this[index]; 104 | oldItem.Collection = null; 105 | _itemSet.Remove(oldItem); 106 | base.RemoveItem(index); 107 | } 108 | 109 | /// 110 | /// 111 | /// 112 | protected override void ClearItems() 113 | { 114 | // Disallow collection modification, so that we don't need to copy it 115 | // when binding the TaskDialogPage. 116 | BoundPage?.DenyIfBound(); 117 | 118 | foreach (TaskDialogRadioButton button in this) 119 | button.Collection = null; 120 | 121 | _itemSet.Clear(); 122 | base.ClearItems(); 123 | } 124 | 125 | private void DenyIfHasOtherCollection(TaskDialogRadioButton item) 126 | { 127 | if (item.Collection != null && item.Collection != this) 128 | throw new InvalidOperationException( 129 | "This control is already part of a different collection."); 130 | } 131 | } -------------------------------------------------------------------------------- /Hourglass/Extensions/DayOfWeekExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Extensions; 8 | 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Globalization; 12 | using System.Linq; 13 | 14 | using Properties; 15 | 16 | /// 17 | /// Provides extensions methods for the enumeration. 18 | /// 19 | public static class DayOfWeekExtensions 20 | { 21 | /// 22 | /// Parses a string into a . 23 | /// 24 | /// A string. 25 | /// An . 26 | /// The parsed from the string. 27 | /// If is not a supported representation of a day of the week. 28 | public static DayOfWeek ParseDayOfWeek(string str, IFormatProvider provider) 29 | { 30 | KeyValuePair[] matches = GetDayOfWeekStrings(provider) 31 | .Where(e => e.Value.StartsWith(str, true /* ignoreCase */, (CultureInfo)provider)) 32 | .ToArray(); 33 | 34 | if (matches.Length != 1) 35 | { 36 | throw new FormatException(); 37 | } 38 | 39 | return matches[0].Key; 40 | } 41 | 42 | /// 43 | /// Returns a string that represents the . 44 | /// 45 | /// A . 46 | /// An to use. 47 | /// A string that represents the current object. 48 | public static string ToLocalizedString(this DayOfWeek dayOfWeek, IFormatProvider provider) 49 | { 50 | IDictionary dayOfWeekStrings = GetDayOfWeekStrings(provider); 51 | return dayOfWeekStrings[dayOfWeek]; 52 | } 53 | 54 | /// 55 | /// Returns a string that represents the . 56 | /// 57 | /// A . 58 | /// An to use. 59 | /// A string that represents the current object. 60 | public static string ToLocalizedString(this DayOfWeek? dayOfWeek, IFormatProvider provider) 61 | { 62 | return dayOfWeek is not null ? dayOfWeek.Value.ToLocalizedString(provider) : string.Empty; 63 | } 64 | 65 | /// 66 | /// Returns a dictionary mapping values to their localized string representations. 67 | /// 68 | /// An . 69 | /// A dictionary mapping values to their localized string representations. 70 | private static IDictionary GetDayOfWeekStrings(IFormatProvider provider) 71 | { 72 | return new Dictionary 73 | { 74 | [DayOfWeek.Monday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsMonday)), 75 | [DayOfWeek.Tuesday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsTuesday)), 76 | [DayOfWeek.Wednesday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsWednesday)), 77 | [DayOfWeek.Thursday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsThursday)), 78 | [DayOfWeek.Friday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsFriday)), 79 | [DayOfWeek.Saturday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsSaturday)), 80 | [DayOfWeek.Sunday] = GetDayOfWeek(nameof(Resources.DayOfWeekExtensionsSunday)) 81 | }; 82 | 83 | string GetDayOfWeek(string name) => 84 | Resources.ResourceManager.GetString(name, provider); 85 | } 86 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogCustomButtonCollection.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | 7 | // ReSharper disable all 8 | 9 | namespace KPreisser.UI; 10 | 11 | /// 12 | /// 13 | /// 14 | public class TaskDialogCustomButtonCollection 15 | : Collection 16 | { 17 | // HashSet to detect duplicate items. 18 | private readonly HashSet _itemSet = 19 | []; 20 | 21 | /// 22 | /// 23 | /// 24 | public TaskDialogCustomButtonCollection() 25 | { 26 | } 27 | 28 | internal TaskDialogPage BoundPage { get; set; } 29 | 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | public TaskDialogCustomButton Add(string text, string descriptionText = null) 37 | { 38 | var button = new TaskDialogCustomButton() 39 | { 40 | Text = text, 41 | DescriptionText = descriptionText 42 | }; 43 | 44 | Add(button); 45 | return button; 46 | } 47 | 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | protected override void SetItem(int index, TaskDialogCustomButton item) 54 | { 55 | // Disallow collection modification, so that we don't need to copy it 56 | // when binding the TaskDialogPage. 57 | BoundPage?.DenyIfBound(); 58 | DenyIfHasOtherCollection(item); 59 | 60 | TaskDialogCustomButton oldItem = this[index]; 61 | if (oldItem != item) 62 | { 63 | // First, add the new item (which will throw if it is a duplicate entry), 64 | // then remove the old one. 65 | if (!_itemSet.Add(item)) 66 | throw new ArgumentException($"Item {item.Text} is already present", nameof(item)); 67 | _itemSet.Remove(oldItem); 68 | 69 | oldItem.Collection = null; 70 | item.Collection = this; 71 | } 72 | 73 | base.SetItem(index, item); 74 | } 75 | 76 | /// 77 | /// 78 | /// 79 | /// 80 | /// 81 | protected override void InsertItem(int index, TaskDialogCustomButton item) 82 | { 83 | // Disallow collection modification, so that we don't need to copy it 84 | // when binding the TaskDialogPage. 85 | BoundPage?.DenyIfBound(); 86 | DenyIfHasOtherCollection(item); 87 | 88 | if (!_itemSet.Add(item)) 89 | throw new ArgumentException($"Item {item.Text} is already present", nameof(item)); 90 | 91 | item.Collection = this; 92 | base.InsertItem(index, item); 93 | } 94 | 95 | /// 96 | /// 97 | /// 98 | /// 99 | protected override void RemoveItem(int index) 100 | { 101 | // Disallow collection modification, so that we don't need to copy it 102 | // when binding the TaskDialogPage. 103 | BoundPage?.DenyIfBound(); 104 | 105 | TaskDialogCustomButton oldItem = this[index]; 106 | oldItem.Collection = null; 107 | _itemSet.Remove(oldItem); 108 | base.RemoveItem(index); 109 | } 110 | 111 | /// 112 | /// 113 | /// 114 | protected override void ClearItems() 115 | { 116 | // Disallow collection modification, so that we don't need to copy it 117 | // when binding the TaskDialogPage. 118 | BoundPage?.DenyIfBound(); 119 | 120 | foreach (TaskDialogCustomButton button in this) 121 | button.Collection = null; 122 | 123 | _itemSet.Clear(); 124 | base.ClearItems(); 125 | } 126 | 127 | private void DenyIfHasOtherCollection(TaskDialogCustomButton item) 128 | { 129 | if (item.Collection != null && item.Collection != this) 130 | throw new InvalidOperationException( 131 | "This control is already part of a different collection."); 132 | } 133 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: [push, pull_request, workflow_dispatch] 8 | 9 | env: 10 | NAME: Hourglass 11 | NAUDIO_ARTIFACT: NAudioHourglassPack 12 | CONFIG: Release 13 | CONFIG_PORTABLE: Release Portable 14 | FW: net481 15 | COMPRESSION_LEVEL: 9 16 | RETENTION_DAYS: 30 17 | 18 | jobs: 19 | build: 20 | runs-on: windows-latest 21 | 22 | steps: 23 | 24 | # Set up 25 | 26 | - name: Setup .NET 27 | uses: actions/setup-dotnet@v5.0.1 28 | with: 29 | dotnet-version: | 30 | 10.0.x 31 | 32 | # Set up msbuild 33 | 34 | - name: Setup msbuild 35 | uses: microsoft/setup-msbuild@v2 36 | 37 | # Check out 38 | 39 | - name: Check out ${{env.NAME}} 40 | uses: actions/checkout@v6.0.0 41 | 42 | # Restore 43 | 44 | - name: Restore ${{env.NAME}}.sln 45 | run: dotnet restore ${{env.NAME}}.sln /p:DisableWarnForInvalidRestoreProjects=true /p:BuildWithNetFrameworkHostedCompiler=true 46 | 47 | # Build 48 | 49 | - name: Build ${{env.NAME}} ${{env.CONFIG_PORTABLE}} 50 | run: msbuild /p:Configuration="${{env.CONFIG_PORTABLE}}" ${{env.NAME}}.sln 51 | 52 | - name: Build ${{env.NAME}} ${{env.CONFIG}} 53 | run: msbuild /p:Configuration="${{env.CONFIG}}" ${{env.NAME}}.sln 54 | 55 | # Test 56 | 57 | - name: Test ${{env.NAME}} ${{env.CONFIG_PORTABLE}} 58 | run: dotnet test --configuration "${{env.CONFIG_PORTABLE}}" --no-build --verbosity normal 59 | 60 | - name: Create ${{env.NAME}} ${{env.CONFIG_PORTABLE}} test report 61 | uses: dorny/test-reporter@v2.2.0 62 | if: ${{success() || failure()}} 63 | with: 64 | name: tests 65 | path: '**/*.trx' 66 | reporter: dotnet-trx 67 | 68 | # Upload artifacts 69 | 70 | - name: Publish ${{env.NAME}} ${{env.CONFIG}} Installer 71 | uses: actions/upload-artifact@v5.0.0 72 | with: 73 | name: ${{env.NAME}}Installer 74 | path: | 75 | ${{env.NAME}}.Bundle/bin/${{env.CONFIG}}/${{env.NAME}}Installer.exe 76 | compression-level: 0 77 | retention-days: ${{env.RETENTION_DAYS}} 78 | 79 | - name: Publish ${{env.NAME}} ${{env.CONFIG_PORTABLE}} 80 | uses: actions/upload-artifact@v5.0.0 81 | with: 82 | name: ${{env.NAME}}Portable 83 | path: | 84 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/${{env.NAME}}.exe 85 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/${{env.NAME}}.exe.config 86 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/ngen-${{env.NAME}}.bat 87 | compression-level: ${{env.COMPRESSION_LEVEL}} 88 | retention-days: ${{env.RETENTION_DAYS}} 89 | 90 | - name: Publish ${{env.NAUDIO_ARTIFACT}} 91 | uses: actions/upload-artifact@v5.0.0 92 | with: 93 | name: ${{env.NAUDIO_ARTIFACT}} 94 | path: | 95 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/ngen-${{env.NAME}}.bat 96 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/Hourglass.NAudio.dll 97 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/Microsoft.Win32.Registry.dll 98 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/NAudio.dll 99 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/NAudio.Core.dll 100 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/NAudio.Vorbis.dll 101 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/NAudio.Wasapi.dll 102 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/NAudio.WinMM.dll 103 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/NVorbis.dll 104 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/System.Buffers.dll 105 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/System.Memory.dll 106 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/System.Numerics.Vectors.dll 107 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/System.Runtime.CompilerServices.Unsafe.dll 108 | ${{env.NAME}}/bin/${{env.CONFIG_PORTABLE}}/${{env.FW}}/System.ValueTuple.dll 109 | compression-level: ${{env.COMPRESSION_LEVEL}} 110 | retention-days: ${{env.RETENTION_DAYS}} 111 | -------------------------------------------------------------------------------- /Hourglass/Managers/KeepAwakeManager.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) Chris Dziemborowicz. All rights reserved. 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace Hourglass.Managers; 8 | 9 | using System.Collections.Generic; 10 | 11 | /// 12 | /// Manages the thread-state of the main user interface thread to keep the computer from sleeping while a timer is running in any window. 13 | /// 14 | public sealed class KeepAwakeManager : Manager 15 | { 16 | /// 17 | /// Singleton instance of the class. 18 | /// 19 | public static readonly KeepAwakeManager Instance = new(); 20 | 21 | /// 22 | /// The set of ids that require that the system to be kept awake. 23 | /// 24 | private readonly HashSet _idsToKeepAwakeFor = []; 25 | 26 | /// 27 | /// The before the manager started keeping the system awake. 28 | /// 29 | private ExecutionState _previousExecutionState = ExecutionState.EsNull; 30 | 31 | /// 32 | /// Prevents a default instance of the class from being created. 33 | /// 34 | private KeepAwakeManager() 35 | { 36 | } 37 | 38 | /// 39 | /// Gets a value indicating whether the manager is currently keeping the system awake. 40 | /// 41 | public bool IsKeepingSystemAwake { get; private set; } 42 | 43 | /// 44 | /// Adds the specified id to the set of ids that require that the system to be kept awake and starts 45 | /// keeping the system awake if it was not already being kept awake. 46 | /// 47 | /// An . 48 | public void StartKeepAwakeFor(int id) 49 | { 50 | _idsToKeepAwakeFor.Add(id); 51 | UpdateKeepAwake(); 52 | } 53 | 54 | /// 55 | /// Removes the specified id from the set of ids that require that the system to be kept awake and stops 56 | /// keeping the system awake if it was being kept awake. 57 | /// 58 | /// An . 59 | public void StopKeepAwakeFor(int id) 60 | { 61 | _idsToKeepAwakeFor.Remove(id); 62 | UpdateKeepAwake(); 63 | } 64 | 65 | /// 66 | protected override void Dispose(bool disposing) 67 | { 68 | if (Disposed) 69 | { 70 | return; 71 | } 72 | 73 | StopKeepAwake(); 74 | base.Dispose(disposing); 75 | } 76 | 77 | /// 78 | /// Starts or stops keeping the system awake as required. 79 | /// 80 | private void UpdateKeepAwake() 81 | { 82 | if (_idsToKeepAwakeFor.Count > 0) 83 | { 84 | StartKeepAwake(); 85 | } 86 | else 87 | { 88 | StopKeepAwake(); 89 | } 90 | } 91 | 92 | /// 93 | /// Start keeping the system awake. If the system is already being kept awake, this method does nothing. 94 | /// 95 | private void StartKeepAwake() 96 | { 97 | if (IsKeepingSystemAwake) 98 | { 99 | return; 100 | } 101 | 102 | ExecutionState executionState = ExecutionState.EsContinuous | ExecutionState.EsDisplayRequired | ExecutionState.EsSystemRequired; 103 | _previousExecutionState = NativeMethods.SetThreadExecutionState(executionState); 104 | 105 | IsKeepingSystemAwake = true; 106 | } 107 | 108 | /// 109 | /// Stops keeping the system awake. If the system is not being kept awake, this method does nothing. 110 | /// 111 | private void StopKeepAwake() 112 | { 113 | if (!IsKeepingSystemAwake) 114 | { 115 | return; 116 | } 117 | 118 | if (_previousExecutionState != ExecutionState.EsNull) 119 | { 120 | NativeMethods.SetThreadExecutionState(_previousExecutionState); 121 | } 122 | 123 | IsKeepingSystemAwake = false; 124 | } 125 | } -------------------------------------------------------------------------------- /Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogControl.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | 5 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 6 | 7 | // ReSharper disable all 8 | 9 | namespace KPreisser.UI; 10 | 11 | /// 12 | /// 13 | /// 14 | public abstract class TaskDialogControl 15 | { 16 | // Disallow inheritance by specifying a private protected constructor. 17 | private protected TaskDialogControl() 18 | { 19 | } 20 | 21 | /// 22 | /// Gets or sets the object that contains data about the control. 23 | /// 24 | public object Tag 25 | { 26 | get; 27 | set; 28 | } 29 | 30 | internal TaskDialogPage BoundPage 31 | { 32 | get; 33 | private set; 34 | } 35 | 36 | /// 37 | /// Gets a value that indicates if the current state of this control 38 | /// allows it to be created in a task dialog when binding it. 39 | /// 40 | internal virtual bool IsCreatable 41 | { 42 | get => true; 43 | } 44 | 45 | /// 46 | /// Gets or sets a value that indicates if this control has been created 47 | /// in a bound task dialog. 48 | /// 49 | internal bool IsCreated 50 | { 51 | get; 52 | private set; 53 | } 54 | 55 | internal TaskDialogFlags Bind(TaskDialogPage page) 56 | { 57 | BoundPage = page ?? 58 | throw new ArgumentNullException(nameof(page)); 59 | 60 | // Use the current value of IsCreatable to determine if the control is 61 | // created. This is important because IsCreatable can change while the 62 | // control is displayed (e.g. if it depends on the Text property). 63 | IsCreated = IsCreatable; 64 | 65 | return IsCreated ? BindCore() : default; 66 | } 67 | 68 | internal void Unbind() 69 | { 70 | if (IsCreated) 71 | UnbindCore(); 72 | 73 | IsCreated = false; 74 | BoundPage = null; 75 | } 76 | 77 | /// 78 | /// Applies initialization after the task dialog is displayed or navigated. 79 | /// 80 | internal void ApplyInitialization() 81 | { 82 | // Only apply the initialization if the control is actually created. 83 | if (IsCreated) 84 | ApplyInitializationCore(); 85 | } 86 | 87 | /// 88 | /// When overridden in a subclass, runs additional binding logic and returns 89 | /// flags to be specified before the task dialog is displayed or navigated. 90 | /// 91 | /// 92 | /// This method will only be called if returns true. 93 | /// 94 | /// 95 | private protected virtual TaskDialogFlags BindCore() 96 | { 97 | return default; 98 | } 99 | 100 | /// 101 | /// 102 | /// 103 | /// 104 | /// This method will only be called if was called. 105 | /// 106 | private protected virtual void UnbindCore() 107 | { 108 | } 109 | 110 | /// 111 | /// When overridden in a subclass, applies initialization after the task dialog 112 | /// is displayed or navigated. 113 | /// 114 | /// 115 | /// This method will only be called if returns true. 116 | /// 117 | private protected virtual void ApplyInitializationCore() 118 | { 119 | } 120 | 121 | private protected void DenyIfBound() 122 | { 123 | BoundPage?.DenyIfBound(); 124 | } 125 | 126 | private protected void DenyIfWaitingForInitialization() 127 | { 128 | BoundPage?.DenyIfWaitingForInitialization(); 129 | } 130 | 131 | private protected void DenyIfNotBoundOrWaitingForInitialization() 132 | { 133 | DenyIfWaitingForInitialization(); 134 | 135 | if (BoundPage == null) 136 | throw new InvalidOperationException( 137 | "This control is not currently bound to a task dialog."); 138 | } 139 | 140 | private protected void DenyIfBoundAndNotCreated() 141 | { 142 | if (BoundPage != null && !IsCreated) 143 | throw new InvalidOperationException("The control has not been created."); 144 | } 145 | } -------------------------------------------------------------------------------- /Hourglass/Windows/ErrorDialog.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | 36 | 37 | 38 | 45 | 46 | 55 | 56 | 57 | 68 | 69 | 70 |