From 0a0b108606d54f0b2d56acfefd8146c16c0723b8 Mon Sep 17 00:00:00 2001 From: James La Novara-Gsell Date: Sat, 28 Dec 2024 16:10:24 -0500 Subject: [PATCH] feat: Option to start AutoCursorLock when Windows starts Closes #27 --- src/AutoCursorLock.App/App.xaml.cs | 22 +++- src/AutoCursorLock.App/MinimizeToTray.cs | 123 +++++++++--------- .../Views/GeneralSettingsWindow.xaml | 34 +++++ .../Views/GeneralSettingsWindow.xaml.cs | 70 ++++++++++ src/AutoCursorLock.App/Views/MainWindow.xaml | 5 +- .../Views/MainWindow.xaml.cs | 48 +++---- 6 files changed, 218 insertions(+), 84 deletions(-) create mode 100644 src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml create mode 100644 src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml.cs diff --git a/src/AutoCursorLock.App/App.xaml.cs b/src/AutoCursorLock.App/App.xaml.cs index 4ef7d81..de5a0db 100644 --- a/src/AutoCursorLock.App/App.xaml.cs +++ b/src/AutoCursorLock.App/App.xaml.cs @@ -6,6 +6,7 @@ namespace AutoCursorLock.App; using AutoCursorLock.App.Views; using Microsoft.Extensions.DependencyInjection; using System; +using System.Linq; using System.Windows; /// @@ -27,6 +28,8 @@ public App() /// public static ServiceProvider Services { get; private set; } = HostingExtensions.CreateContainer(); + private MinimizeToTray? minimizeToTray; + /// /// Handles unhandled exceptions. /// @@ -54,7 +57,24 @@ protected override async void OnStartup(StartupEventArgs e) { var mainWindowFactory = Services.GetRequiredService(); var mainWindow = await mainWindowFactory.CreateAsync(); - mainWindow.Show(); + + this.minimizeToTray = new MinimizeToTray(mainWindow); + + // get arguments + var minimizeArg = e.Args.FirstOrDefault(arg => arg == "--minimize") is not null; + if (minimizeArg) + { + var quietArg = e.Args.FirstOrDefault(arg => arg == "--quiet") is not null; + + mainWindow.WindowState = WindowState.Minimized; + this.minimizeToTray.UpdateTrayState(showBalloon: !quietArg); + } + else + { + mainWindow.Show(); + } + + this.minimizeToTray.StartWatching(); base.OnStartup(e); } diff --git a/src/AutoCursorLock.App/MinimizeToTray.cs b/src/AutoCursorLock.App/MinimizeToTray.cs index 96c98cc..342b93b 100644 --- a/src/AutoCursorLock.App/MinimizeToTray.cs +++ b/src/AutoCursorLock.App/MinimizeToTray.cs @@ -13,86 +13,89 @@ namespace AutoCursorLock /// /// Class implementing support for "minimize to tray" functionality. /// - public static class MinimizeToTray + public class MinimizeToTray { + private readonly Window window; + private NotifyIcon? notifyIcon; + private bool balloonShown; + /// - /// Enables "minimize to tray" behavior for the specified Window. + /// Initializes a new instance of the class. /// - /// Window to enable the behavior for. - public static void Enable(Window window) + /// Window instance to attach to. + public MinimizeToTray(Window window) + { + Debug.Assert(window != null, "window parameter is null."); + this.window = window; + } + + public void StartWatching() { - // No need to track this instance; its event handlers will keep it alive - new MinimizeToTrayInstance(window); + this.window.StateChanged += new EventHandler(HandleStateChanged); } /// - /// Class implementing "minimize to tray" functionality for a Window instance. + /// Handles the Window's StateChanged event. /// - private class MinimizeToTrayInstance + /// Event source. + /// Event arguments. + private void HandleStateChanged(object? sender, EventArgs e) { - private Window window; - private NotifyIcon? notifyIcon; - private bool balloonShown; - - /// - /// Initializes a new instance of the class. - /// - /// Window instance to attach to. - public MinimizeToTrayInstance(Window window) - { - Debug.Assert(window != null, "window parameter is null."); - this.window = window; - this.window.StateChanged += new EventHandler(HandleStateChanged); - } + UpdateTrayState(showBalloon: true); + } - /// - /// Handles the Window's StateChanged event. - /// - /// Event source. - /// Event arguments. - private void HandleStateChanged(object? sender, EventArgs e) + public void UpdateTrayState(bool showBalloon = true) + { + if (this.notifyIcon == null) { - if (this.notifyIcon == null) - { - var icon = Assembly.GetEntryAssembly()?.Location; - - // Initialize NotifyIcon instance "on demand" - this.notifyIcon = new NotifyIcon(); + var icon = Assembly.GetEntryAssembly()?.Location; - if (icon != null) - { - this.notifyIcon.Icon = Icon.ExtractAssociatedIcon(icon); - } + // Initialize NotifyIcon instance "on demand" + this.notifyIcon = new NotifyIcon(); - this.notifyIcon.MouseClick += new MouseEventHandler(HandleNotifyIconOrBalloonClicked); - this.notifyIcon.BalloonTipClicked += new EventHandler(HandleNotifyIconOrBalloonClicked); + if (icon != null) + { + this.notifyIcon.Icon = Icon.ExtractAssociatedIcon(icon); } - // Update copy of Window Title in case it has changed - this.notifyIcon.Text = this.window.Title; + this.notifyIcon.MouseClick += new MouseEventHandler(HandleNotifyIconOrBalloonClicked); + this.notifyIcon.BalloonTipClicked += new EventHandler(HandleNotifyIconOrBalloonClicked); + } - // Show/hide Window and NotifyIcon - var minimized = this.window.WindowState == WindowState.Minimized; - this.window.ShowInTaskbar = !minimized; - this.notifyIcon.Visible = minimized; - if (minimized && !this.balloonShown) - { - // If this is the first time minimizing to the tray, show the user what happened - this.notifyIcon.ShowBalloonTip(1000, this.window.Title, "Minimized to tray...", ToolTipIcon.None); - this.balloonShown = true; - } + // Update copy of Window Title in case it has changed + this.notifyIcon.Text = this.window.Title; + + // Show/hide Window and NotifyIcon + var minimized = this.window.WindowState == WindowState.Minimized; + this.window.ShowInTaskbar = !minimized; + this.notifyIcon.Visible = minimized; + if (showBalloon && minimized && !this.balloonShown) + { + // If this is the first time minimizing to the tray, show the user what happened + this.notifyIcon.ShowBalloonTip(1000, this.window.Title, "Minimized to tray...", ToolTipIcon.None); + this.balloonShown = true; } + } - /// - /// Handles a click on the notify icon or its balloon. - /// - /// Event source. - /// Event arguments. - private void HandleNotifyIconOrBalloonClicked(object? sender, EventArgs e) + /// + /// Handles a click on the notify icon or its balloon. + /// + /// Event source. + /// Event arguments. + private void HandleNotifyIconOrBalloonClicked(object? sender, EventArgs e) + { + // Restore the Window + this.window.WindowState = WindowState.Normal; + + // If the program was started with the --minimize argument, the Window has not been shown yet. + // Show the program and update the tray state one time since the StateChanged event does not fire on show. + if (!this.window.IsLoaded) { - // Restore the Window - this.window.WindowState = WindowState.Normal; + this.window.Show(); + UpdateTrayState(); } + + this.window.Activate(); } } } diff --git a/src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml b/src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml new file mode 100644 index 0000000..32da081 --- /dev/null +++ b/src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + Automatically start the application when Windows starts + + + + + diff --git a/src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml.cs b/src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml.cs new file mode 100644 index 0000000..982d1a2 --- /dev/null +++ b/src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml.cs @@ -0,0 +1,70 @@ +// Copyright(c) James La Novara-Gsell. All Rights Reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace AutoCursorLock.App.Views; + +using System; +using System.Diagnostics; +using System.Windows; + +/// +/// Interaction logic for GeneralSettings.xaml. +/// +public partial class GeneralSettingsWindow : Window +{ + private readonly string shortcutPath; + + /// + /// Initializes a new instance of the class. + /// + public GeneralSettingsWindow() + { + InitializeComponent(); + + // check if the shortcut exists in the startup folder + var startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + this.shortcutPath = System.IO.Path.Combine(startupFolder, "AutoCursorLock.lnk"); + StartWithWindows = System.IO.File.Exists(this.shortcutPath); + + this.mainGrid.DataContext = this; + } + + /// + /// Gets or sets a value indicating whether to start the application when Windows starts. + /// + public bool StartWithWindows { get; set; } + + private void StartWithWindowsCheckBox_Checked(object sender, RoutedEventArgs e) + { + if (System.IO.File.Exists(this.shortcutPath)) + { + return; + } + + // create a shortcut in the startup folder + var shortcutPath = this.shortcutPath; + var targetPath = Process.GetCurrentProcess().MainModule?.FileName ?? throw new AutoCursorLockException("Could not get entry assembly"); + + var shellType = Type.GetTypeFromProgID("WScript.Shell") ?? throw new AutoCursorLockException("Could not get WScript.Shell type"); + + // The use of dynamic here is unfortunate, but using the Activator removes a dependency on the IWshRuntimeLibrary + // and even if we used the full library, the IWshShell3.CreateShortctut method returns a dynamic... + dynamic shell = Activator.CreateInstance(shellType) ?? throw new AutoCursorLockException("Could not create WScript.Shell instance"); + + var shortcut = shell.CreateShortcut(shortcutPath); + shortcut.Description = "AutoCursorLock"; + shortcut.TargetPath = targetPath; + shortcut.Arguments = "--minimize --quiet"; + shortcut.Save(); + } + + private void StartWithWindowsCheckBox_Unchecked(object sender, RoutedEventArgs e) + { + // delete the shortcut in the startup folder + var shortcutPath = this.shortcutPath; + if (System.IO.File.Exists(shortcutPath)) + { + System.IO.File.Delete(shortcutPath); + } + } +} diff --git a/src/AutoCursorLock.App/Views/MainWindow.xaml b/src/AutoCursorLock.App/Views/MainWindow.xaml index 6cb1d17..0492254 100644 --- a/src/AutoCursorLock.App/Views/MainWindow.xaml +++ b/src/AutoCursorLock.App/Views/MainWindow.xaml @@ -8,7 +8,6 @@ xmlns:localExtensions="clr-namespace:AutoCursorLock.App.Extensions" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:models="clr-namespace:AutoCursorLock.App.Models" xmlns:sdk="clr-namespace:AutoCursorLock.Sdk.Models;assembly=AutoCursorLock.Sdk" - Loaded="Window_Loaded" mc:Ignorable="d" Title="AutoCursorLock" Height="620" @@ -39,6 +38,10 @@ + + + diff --git a/src/AutoCursorLock.App/Views/MainWindow.xaml.cs b/src/AutoCursorLock.App/Views/MainWindow.xaml.cs index 92a68b1..b63e2e9 100644 --- a/src/AutoCursorLock.App/Views/MainWindow.xaml.cs +++ b/src/AutoCursorLock.App/Views/MainWindow.xaml.cs @@ -31,7 +31,7 @@ public partial class MainWindow : Window, INotifyPropertyChanged private readonly ILogger logger; private readonly ApplicationEventSource applicationEventSource; - private readonly object activeProcessLock = new (); + private readonly object activeProcessLock = new(); private bool globalLockEnabled = true; private bool applicationLockEnabled = false; @@ -60,10 +60,11 @@ public MainWindow( this.mainGrid.DataContext = this; - MinimizeToTray.Enable(this); + UserSettings = userSettings; + // create a window handle to start monitoring for hotkey presses and application focus changes this.windowInteropHelper = new WindowInteropHelper(this); - UserSettings = userSettings; + this.windowInteropHelper.EnsureHandle(); } /// @@ -170,8 +171,23 @@ public ProcessListItem? ActiveProcess protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); - var source = (HwndSource)PresentationSource.FromVisual(this); + var source = (HwndSource)HwndSource.FromHwnd(this.windowInteropHelper.EnsureHandle()); source.AddHook(WndProc); + + RegisterHotKey(); + + var success = this.applicationEventSource.Register(); + this.logger.LogDebug("Register application hook: {Success}", success); + + if (!success) + { + var errorCode = Marshal.GetLastWin32Error(); + this.logger.LogError("Application hook error code {ErrorCode}", errorCode); + } + + this.applicationEventSource.ApplicationChanged += OnApplicationChanged; + + this.applicationEventSource.Update(); } /// @@ -286,24 +302,6 @@ private void RegisterHotKey() } } - private void Window_Loaded(object sender, RoutedEventArgs e) - { - RegisterHotKey(); - - var success = this.applicationEventSource.Register(); - this.logger.LogDebug("Register application hook: {Success}", success); - - if (!success) - { - var errorCode = Marshal.GetLastWin32Error(); - this.logger.LogError("Application hook error code {ErrorCode}", errorCode); - } - - this.applicationEventSource.ApplicationChanged += OnApplicationChanged; - - this.applicationEventSource.Update(); - } - private void RemoveButton_Click(object sender, RoutedEventArgs e) { var processItem = (ProcessListItem)this.enabledProcessList.SelectedItem; @@ -399,4 +397,10 @@ private void AppLockSettingsButton_Click(object sender, RoutedEventArgs e) var appLockSettingsWindow = new AppLockSettingsWindow(processItem); appLockSettingsWindow.ShowDialog(); } + + private void SettingsItem_Click(object sender, RoutedEventArgs e) + { + var settingsWindow = new GeneralSettingsWindow(); + settingsWindow.ShowDialog(); + } }