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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/AutoCursorLock.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using AutoCursorLock.App.Views;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Windows;

/// <summary>
Expand All @@ -27,6 +28,8 @@
/// </summary>
public static ServiceProvider Services { get; private set; } = HostingExtensions.CreateContainer();

private MinimizeToTray? minimizeToTray;

Check warning on line 31 in src/AutoCursorLock.App/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (Release)


/// <summary>
/// Handles unhandled exceptions.
/// </summary>
Expand Down Expand Up @@ -54,7 +57,24 @@
{
var mainWindowFactory = Services.GetRequiredService<MainWindowFactory>();
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);
}
Expand Down
123 changes: 63 additions & 60 deletions src/AutoCursorLock.App/MinimizeToTray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,86 +13,89 @@
/// <summary>
/// Class implementing support for "minimize to tray" functionality.
/// </summary>
public static class MinimizeToTray
public class MinimizeToTray
{
private readonly Window window;
private NotifyIcon? notifyIcon;
private bool balloonShown;

/// <summary>
/// Enables "minimize to tray" behavior for the specified Window.
/// Initializes a new instance of the <see cref="MinimizeToTray"/> class.
/// </summary>
/// <param name="window">Window to enable the behavior for.</param>
public static void Enable(Window window)
/// <param name="window">Window instance to attach to.</param>
public MinimizeToTray(Window window)
{
Debug.Assert(window != null, "window parameter is null.");
this.window = window;
}

public void StartWatching()

Check warning on line 32 in src/AutoCursorLock.App/MinimizeToTray.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Missing XML comment for publicly visible type or member 'MinimizeToTray.StartWatching()'
{
// No need to track this instance; its event handlers will keep it alive
new MinimizeToTrayInstance(window);
this.window.StateChanged += new EventHandler(HandleStateChanged);
}

/// <summary>
/// Class implementing "minimize to tray" functionality for a Window instance.
/// Handles the Window's StateChanged event.
/// </summary>
private class MinimizeToTrayInstance
/// <param name="sender">Event source.</param>
/// <param name="e">Event arguments.</param>
private void HandleStateChanged(object? sender, EventArgs e)
{
private Window window;
private NotifyIcon? notifyIcon;
private bool balloonShown;

/// <summary>
/// Initializes a new instance of the <see cref="MinimizeToTrayInstance"/> class.
/// </summary>
/// <param name="window">Window instance to attach to.</param>
public MinimizeToTrayInstance(Window window)
{
Debug.Assert(window != null, "window parameter is null.");
this.window = window;
this.window.StateChanged += new EventHandler(HandleStateChanged);
}
UpdateTrayState(showBalloon: true);
}

/// <summary>
/// Handles the Window's StateChanged event.
/// </summary>
/// <param name="sender">Event source.</param>
/// <param name="e">Event arguments.</param>
private void HandleStateChanged(object? sender, EventArgs e)
public void UpdateTrayState(bool showBalloon = true)

Check warning on line 47 in src/AutoCursorLock.App/MinimizeToTray.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Missing XML comment for publicly visible type or member 'MinimizeToTray.UpdateTrayState(bool)'

Check warning on line 47 in src/AutoCursorLock.App/MinimizeToTray.cs

View workflow job for this annotation

GitHub Actions / build (Release)

{
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;
}
}

/// <summary>
/// Handles a click on the notify icon or its balloon.
/// </summary>
/// <param name="sender">Event source.</param>
/// <param name="e">Event arguments.</param>
private void HandleNotifyIconOrBalloonClicked(object? sender, EventArgs e)
/// <summary>
/// Handles a click on the notify icon or its balloon.
/// </summary>
/// <param name="sender">Event source.</param>
/// <param name="e">Event arguments.</param>
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();
}
}
}
34 changes: 34 additions & 0 deletions src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Window x:Class="AutoCursorLock.App.Views.GeneralSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AutoCursorLock.App.Views"
mc:Ignorable="d"
Title="General Settings" Height="450" Width="800">
<Grid x:Name="mainGrid" ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<TextBlock
Grid.Column="0"
Grid.Row="0"
Margin="5">
Automatically start the application when Windows starts
</TextBlock>
<CheckBox
Grid.Column="1"
Grid.Row="0"
Margin="5"
IsChecked="{Binding StartWithWindows}"
Checked="StartWithWindowsCheckBox_Checked"
Unchecked="StartWithWindowsCheckBox_Unchecked"/>

</Grid>
</Window>
70 changes: 70 additions & 0 deletions src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright(c) James La Novara-Gsell. All Rights Reserved.

Check warning on line 1 in src/AutoCursorLock.App/Views/GeneralSettingsWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build (Release)

The file header copyright text should match the copyright text from the settings. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1636.md)
// 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;

/// <summary>
/// Interaction logic for GeneralSettings.xaml.
/// </summary>
public partial class GeneralSettingsWindow : Window
{
private readonly string shortcutPath;

/// <summary>
/// Initializes a new instance of the <see cref="GeneralSettingsWindow"/> class.
/// </summary>
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;
}

/// <summary>
/// Gets or sets a value indicating whether to start the application when Windows starts.
/// </summary>
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);
}
}
}
5 changes: 4 additions & 1 deletion src/AutoCursorLock.App/Views/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -39,6 +38,10 @@
</Grid.ColumnDefinitions>

<Menu Grid.Row="0">
<MenuItem Header="_File">
<MenuItem Header="_Settings"
Click="SettingsItem_Click"/>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About"
Click="AboutItem_Click"/>
Expand Down
Loading
Loading