├── 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 |
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 |
76 |
77 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogFooter.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 |
3 | using System;
4 |
5 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS;
6 | using TaskDialogIconElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ICON_ELEMENTS;
7 | using TaskDialogTextElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ELEMENTS;
8 |
9 | // ReSharper disable all
10 |
11 | namespace KPreisser.UI;
12 |
13 | ///
14 | ///
15 | ///
16 | public sealed class TaskDialogFooter : TaskDialogControl
17 | {
18 | private string _text;
19 |
20 | private TaskDialogIcon _icon;
21 |
22 | private bool _boundIconIsFromHandle;
23 |
24 | ///
25 | ///
26 | ///
27 | public TaskDialogFooter()
28 | {
29 | }
30 |
31 | ///
32 | ///
33 | ///
34 | ///
35 | public TaskDialogFooter(string text)
36 | : this()
37 | {
38 | _text = text;
39 | }
40 |
41 | ///
42 | /// Gets or sets the text to be displayed in the dialog's footer area.
43 | ///
44 | ///
45 | /// This property can be set while the dialog is shown.
46 | ///
47 | public string Text
48 | {
49 | get => _text;
50 |
51 | set
52 | {
53 | DenyIfBoundAndNotCreated();
54 | DenyIfWaitingForInitialization();
55 |
56 | // Update the text if we are bound.
57 | BoundPage?.BoundTaskDialog.UpdateTextElement(
58 | TaskDialogTextElement.TDE_FOOTER,
59 | value);
60 |
61 | _text = value;
62 | }
63 | }
64 |
65 | ///
66 | /// Gets or sets the footer icon.
67 | ///
68 | ///
69 | /// This property can be set while the dialog is shown (but in that case, it
70 | /// cannot be switched between instances of
71 | /// and instances of other icon types).
72 | ///
73 | public TaskDialogIcon Icon
74 | {
75 | get => _icon;
76 |
77 | set
78 | {
79 | DenyIfBoundAndNotCreated();
80 | DenyIfWaitingForInitialization();
81 |
82 | (IntPtr iconValue, bool? iconIsFromHandle) =
83 | TaskDialogPage.GetIconValue(value);
84 |
85 | // The native task dialog icon cannot be updated from a handle
86 | // type to a non-handle type and vice versa, so we need to throw
87 | // throw in such a case.
88 | if (BoundPage != null &&
89 | iconIsFromHandle != null &&
90 | iconIsFromHandle != _boundIconIsFromHandle)
91 | throw new InvalidOperationException(
92 | "Cannot update the icon from a handle icon type to a " +
93 | "non-handle icon type, and vice versa.");
94 |
95 | BoundPage?.BoundTaskDialog.UpdateIconElement(
96 | TaskDialogIconElement.TDIE_ICON_FOOTER,
97 | iconValue);
98 |
99 | _icon = value;
100 | }
101 | }
102 |
103 | internal override bool IsCreatable
104 | {
105 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text);
106 | }
107 |
108 | ///
109 | ///
110 | ///
111 | ///
112 | public override string ToString()
113 | {
114 | return _text ?? base.ToString();
115 | }
116 |
117 | internal TaskDialogFlags Bind(TaskDialogPage page, out IntPtr footerIconValue)
118 | {
119 | TaskDialogFlags result = base.Bind(page);
120 |
121 | footerIconValue = TaskDialogPage.GetIconValue(_icon).iconValue;
122 |
123 | return result;
124 | }
125 |
126 | private protected override TaskDialogFlags BindCore()
127 | {
128 | TaskDialogFlags flags = base.BindCore();
129 |
130 | _boundIconIsFromHandle = TaskDialogPage.GetIconValue(_icon).iconIsFromHandle
131 | ?? false;
132 |
133 | if (_boundIconIsFromHandle)
134 | flags |= TaskDialogFlags.TDF_USE_HICON_FOOTER;
135 |
136 | return flags;
137 | }
138 |
139 | private protected override void UnbindCore()
140 | {
141 | _boundIconIsFromHandle = false;
142 |
143 | base.UnbindCore();
144 | }
145 | }
--------------------------------------------------------------------------------
/Hourglass/Lib/TaskDialog/TaskDialog/TaskDialogExpander.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 |
3 | using System;
4 |
5 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS;
6 | using TaskDialogTextElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ELEMENTS;
7 |
8 | #pragma warning disable S1135
9 |
10 | // ReSharper disable all
11 |
12 | namespace KPreisser.UI;
13 |
14 | ///
15 | ///
16 | ///
17 | public sealed class TaskDialogExpander : TaskDialogControl
18 | {
19 | private string _text;
20 |
21 | private string _expandedButtonText;
22 |
23 | private string _collapsedButtonText;
24 |
25 | private bool _expandFooterArea;
26 |
27 | private bool _expanded;
28 |
29 | ///
30 | ///
31 | ///
32 | public event EventHandler ExpandedChanged;
33 |
34 | ///
35 | ///
36 | ///
37 | public TaskDialogExpander()
38 | {
39 | }
40 |
41 | ///
42 | ///
43 | ///
44 | ///
45 | public TaskDialogExpander(string text)
46 | : this()
47 | {
48 | _text = text;
49 | }
50 |
51 | ///
52 | /// Gets or sets the text to be displayed in the dialog's expanded area.
53 | ///
54 | ///
55 | /// This property can be set while the dialog is shown.
56 | ///
57 | public string Text
58 | {
59 | get => _text;
60 |
61 | set
62 | {
63 | DenyIfBoundAndNotCreated();
64 | DenyIfWaitingForInitialization();
65 |
66 | // Update the text if we are bound.
67 | BoundPage?.BoundTaskDialog.UpdateTextElement(
68 | TaskDialogTextElement.TDE_EXPANDED_INFORMATION,
69 | value);
70 |
71 | _text = value;
72 | }
73 | }
74 |
75 | ///
76 | ///
77 | ///
78 | public string ExpandedButtonText
79 | {
80 | get => _expandedButtonText;
81 |
82 | set
83 | {
84 | DenyIfBound();
85 |
86 | _expandedButtonText = value;
87 | }
88 | }
89 |
90 | ///
91 | ///
92 | ///
93 | public string CollapsedButtonText
94 | {
95 | get => _collapsedButtonText;
96 |
97 | set
98 | {
99 | DenyIfBound();
100 |
101 | _collapsedButtonText = value;
102 | }
103 | }
104 |
105 | ///
106 | ///
107 | ///
108 | public bool Expanded
109 | {
110 | get => _expanded;
111 |
112 | set
113 | {
114 | // The Task Dialog doesn't provide a message type to click the expando
115 | // button, so we don't allow to change this property (it will however
116 | // be updated when we receive an ExpandoButtonClicked notification).
117 | // TODO: Should we throw only if the new value is different than the
118 | // old one?
119 | DenyIfBound();
120 |
121 | _expanded = value;
122 | }
123 | }
124 |
125 | ///
126 | ///
127 | ///
128 | public bool ExpandFooterArea
129 | {
130 | get => _expandFooterArea;
131 |
132 | set
133 | {
134 | DenyIfBound();
135 |
136 | _expandFooterArea = value;
137 | }
138 | }
139 |
140 | internal override bool IsCreatable
141 | {
142 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text);
143 | }
144 |
145 | ///
146 | ///
147 | ///
148 | ///
149 | public override string ToString()
150 | {
151 | return _text ?? base.ToString();
152 | }
153 |
154 | internal void HandleExpandoButtonClicked(bool expanded)
155 | {
156 | _expanded = expanded;
157 | OnExpandedChanged(EventArgs.Empty);
158 | }
159 |
160 | private protected override TaskDialogFlags BindCore()
161 | {
162 | TaskDialogFlags flags = base.BindCore();
163 |
164 | if (_expanded)
165 | flags |= TaskDialogFlags.TDF_EXPANDED_BY_DEFAULT;
166 | if (_expandFooterArea)
167 | flags |= TaskDialogFlags.TDF_EXPAND_FOOTER_AREA;
168 |
169 | return flags;
170 | }
171 |
172 | private void OnExpandedChanged(EventArgs e)
173 | {
174 | ExpandedChanged?.Invoke(this, e);
175 | }
176 | }
--------------------------------------------------------------------------------
/Hourglass/Windows/UsageDialog.xaml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
37 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/Hourglass/Windows/UsageDialog.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.Windows;
11 | using System.Windows.Media;
12 | using System.Windows.Navigation;
13 |
14 | using Extensions;
15 |
16 | // ReSharper disable MismatchedFileName
17 |
18 | ///
19 | /// A window that displays command-line usage.
20 | ///
21 | public sealed partial class UsageDialog
22 | {
23 | private static UsageDialog? _instance;
24 |
25 | ///
26 | /// Initializes a new instance of the class.
27 | ///
28 | public UsageDialog()
29 | {
30 | InitializeComponent();
31 | InitializeMaxSize();
32 | }
33 |
34 | ///
35 | /// Gets or sets an optional error message to be displayed.
36 | ///
37 | public string? ErrorMessage { get; set; }
38 |
39 | ///
40 | /// Initializes the and properties.
41 | ///
42 | private void InitializeMaxSize()
43 | {
44 | MaxWidth = 0.75 * SystemParameters.WorkArea.Width;
45 | MaxHeight = 0.75 * SystemParameters.WorkArea.Height;
46 | }
47 |
48 | public static void ShowOrActivate(string? errorMessage = null)
49 | {
50 | if (_instance is not null)
51 | {
52 | _instance.Activate();
53 | return;
54 | }
55 |
56 | _instance = new()
57 | {
58 | ErrorMessage = errorMessage
59 | };
60 |
61 | try
62 | {
63 | if (Application.Current?.Dispatcher is null)
64 | {
65 | _instance.ShowDialog();
66 | }
67 | else
68 | {
69 | _instance.Show();
70 | }
71 | }
72 | catch (Exception ex)
73 | {
74 | // Might be thrown when running non-elevated Hourglass instance receives request from the elevated Hourglass instance.
75 | MessageBox.Show(ex.Message, Properties.Resources.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
76 | }
77 | }
78 |
79 | private void UsageDialogClosed(object sender, EventArgs e)
80 | {
81 | _instance = null;
82 | }
83 |
84 | ///
85 | protected override void OnActivated(EventArgs e)
86 | {
87 | Application.Current.ClearJumpList();
88 |
89 | base.OnActivated(e);
90 | }
91 |
92 | ///
93 | /// Invoked when the window is laid out, rendered, and ready for interaction.
94 | ///
95 | /// The window.
96 | /// The event data.
97 | private void WindowLoaded(object sender, RoutedEventArgs e)
98 | {
99 | if (!string.IsNullOrWhiteSpace(ErrorMessage))
100 | {
101 | MessageTextBlock.Background = new SolidColorBrush(Color.FromRgb(199, 80, 80));
102 | MessageTextBlock.Text = ErrorMessage;
103 | }
104 | else
105 | {
106 | MessageTextBlock.Background = Brushes.Gray;
107 | MessageTextBlock.Text = Properties.Resources.UsageDialogDefaultMessageText;
108 | }
109 |
110 | Activate();
111 | }
112 |
113 | ///
114 | /// Invoked when the "About Hourglass" hyperlink is clicked.
115 | ///
116 | /// The "About Hourglass" hyperlink.
117 | /// The event data.
118 | private void AboutHourglassHyperlinkClick(object sender, RoutedEventArgs e)
119 | {
120 | AboutDialog.ShowOrActivate();
121 | }
122 |
123 | ///
124 | /// Invoked when the close button is clicked.
125 | ///
126 | /// The close button.
127 | /// The event data.
128 | private void CloseButtonClick(object sender, RoutedEventArgs e)
129 | {
130 | Close();
131 | }
132 |
133 | private void HyperlinkRequestNavigate(object sender, RequestNavigateEventArgs e)
134 | {
135 | e.Uri.Navigate();
136 | }
137 | }
--------------------------------------------------------------------------------
/Hourglass/Windows/AboutDialog.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.Text.RegularExpressions;
11 | using System.Windows;
12 | using System.Windows.Navigation;
13 |
14 | using Extensions;
15 | using Managers;
16 |
17 | // ReSharper disable ExceptionNotDocumented
18 | // ReSharper disable MismatchedFileName
19 |
20 | ///
21 | /// A window that displays information about the app.
22 | ///
23 | public sealed partial class AboutDialog
24 | {
25 | ///
26 | /// The instance of the that is showing, or null if there is no instance showing.
27 | ///
28 | private static AboutDialog? _instance;
29 |
30 | ///
31 | /// The application name.
32 | ///
33 | public static readonly string AppName =
34 | #if PORTABLE
35 | Properties.Resources.AppNamePortable;
36 | #else
37 | Properties.Resources.AppName;
38 | #endif
39 |
40 | ///
41 | /// Initializes a new instance of the class.
42 | ///
43 | public AboutDialog()
44 | {
45 | InitializeComponent();
46 | InitializeMaxSize();
47 | }
48 |
49 | ///
50 | /// The application license.
51 | ///
52 | public static readonly string License = $"{Environment.NewLine}{Properties.Resources.License}{Environment.NewLine}";
53 |
54 | ///
55 | /// The application copyright.
56 | ///
57 | public static readonly string Copyright = Regex.Match(License, @"Copyright[^\r\n]+").Value;
58 |
59 | ///
60 | /// The application version.
61 | ///
62 | public static string Version
63 | {
64 | get
65 | {
66 | Version version = UpdateManager.CurrentVersion;
67 |
68 | return version.Revision != 0
69 | ? version.ToString()
70 | : version.ToString(version.Build != 0 ? 3 : 2);
71 | }
72 | }
73 |
74 | ///
75 | /// Shows or activates the . Call this method instead of the constructor to prevent
76 | /// multiple instances of the dialog.
77 | ///
78 | public static void ShowOrActivate()
79 | {
80 | if (_instance is null)
81 | {
82 | _instance = new();
83 | _instance.Show();
84 | }
85 | else
86 | {
87 | _instance.Activate();
88 | }
89 | }
90 |
91 | ///
92 | protected override void OnActivated(EventArgs e)
93 | {
94 | Application.Current.ClearJumpList();
95 |
96 | base.OnActivated(e);
97 | }
98 |
99 | ///
100 | /// Initializes the and properties.
101 | ///
102 | private void InitializeMaxSize()
103 | {
104 | MaxWidth = 0.75 * SystemParameters.WorkArea.Width;
105 | MaxHeight = 0.75 * SystemParameters.WorkArea.Height;
106 | }
107 |
108 | private void AboutDialogClosed(object sender, EventArgs e)
109 | {
110 | #pragma warning disable S2696
111 | _instance = null;
112 | #pragma warning restore S2696
113 | }
114 |
115 | ///
116 | /// Invoked when navigation events are requested.
117 | ///
118 | /// The hyperlink requesting navigation.
119 | /// The event data.
120 | private void HyperlinkRequestNavigate(object sender, RequestNavigateEventArgs e)
121 | {
122 | e.Uri.Navigate();
123 | }
124 |
125 |
126 | private void UsageHyperlinkClick(object sender, RoutedEventArgs e)
127 | {
128 | UsageDialog.ShowOrActivate();
129 | }
130 |
131 | ///
132 | /// Invoked when the close button is clicked.
133 | ///
134 | /// The close button.
135 | /// The event data.
136 | private void CloseButtonClick(object sender, RoutedEventArgs e)
137 | {
138 | Close();
139 | }
140 | }
--------------------------------------------------------------------------------