From 8930f1d614c4017132782d6c8a98ce07e760aa0b Mon Sep 17 00:00:00 2001 From: Vladimir Eremeev Date: Mon, 13 Jan 2025 09:44:40 +0300 Subject: [PATCH] System tray icon Add code for creation of system tray icon, menu and handle menu events System tray icon - add code for creation of system tray icon, menu and handle menu events - add option to preferences dialog - add a kind of a single instance mode: only first launched instance creates system tray icon and does not quit --- src/App.Commands.cs | 3 +- src/App.axaml.cs | 54 +++++++++++++++++++++++++++++-- src/Native/Linux.cs | 21 ++++++++++++ src/Native/MacOS.cs | 2 ++ src/Native/OS.cs | 7 ++++ src/Native/Windows.cs | 22 +++++++++++++ src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/ru_RU.axaml | 1 + src/ViewModels/Launcher.cs | 3 +- src/ViewModels/Preferences.cs | 8 +++++ src/Views/Launcher.axaml.cs | 8 ++++- src/Views/Preferences.axaml | 33 +++++++++++-------- src/Views/Preferences.axaml.cs | 11 +++++++ 13 files changed, 155 insertions(+), 19 deletions(-) diff --git a/src/App.Commands.cs b/src/App.Commands.cs index 85a75829..84fbbe87 100644 --- a/src/App.Commands.cs +++ b/src/App.Commands.cs @@ -36,7 +36,8 @@ namespace SourceGit #endif } } - + + public static readonly Command Unminimize = new Command(_ => ShowWindow()); public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences())); public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys())); public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir)); diff --git a/src/App.axaml.cs b/src/App.axaml.cs index cca9f2ea..e03a8050 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -14,6 +14,8 @@ using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Fonts; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; @@ -167,6 +169,46 @@ namespace SourceGit } } + public void SetupTrayIcon(bool enable) + { + if (enable && Native.OS.EnsureSingleInstance()) + { + var icons = new TrayIcons { + new TrayIcon { + Icon = new WindowIcon(new Bitmap(AssetLoader.Open(new Uri("avares://SourceGit/App.ico")))), + Menu = [ + new NativeMenuItem(Text("Open")) {Command = Unminimize}, + new NativeMenuItem(Text("Preferences")) {Command = OpenPreferencesCommand}, + new NativeMenuItemSeparator(), + new NativeMenuItem(Text("Quit")) {Command = QuitCommand}, + ] + } + }; + icons[0].Clicked += (_, _) => ToggleWindow(); + TrayIcon.SetIcons(Current, icons); + _createdSystemTrayIcon = true; + } + } + + private static void ToggleWindow() { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + if (desktop.MainWindow.IsVisible) { + desktop.MainWindow.Hide(); + } else { + ShowWindow(); + } + } + } + + private static void ShowWindow() + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + desktop.MainWindow.WindowState = WindowState.Normal; + desktop.MainWindow.Show(); + desktop.MainWindow.BringIntoView(); + desktop.MainWindow.Focus(); + } + } public static void SetFonts(string defaultFont, string monospaceFont, bool onlyUseMonospaceFontInEditor) { var app = Current as App; @@ -320,6 +362,7 @@ namespace SourceGit TryLaunchAsNormal(desktop); } + base.OnFrameworkInitializationCompleted(); } #endregion @@ -476,11 +519,17 @@ namespace SourceGit if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) startupRepo = desktop.Args[0]; - _launcher = new ViewModels.Launcher(startupRepo); + var pref = ViewModels.Preferences.Instance; + + SetupTrayIcon(pref.SystemTrayIcon); + if (_createdSystemTrayIcon) { + desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; + } + + _launcher = new ViewModels.Launcher(startupRepo) { InterceptQuit = _createdSystemTrayIcon }; desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; #if !DISABLE_UPDATE_DETECTION - var pref = ViewModels.Preferences.Instance; if (pref.ShouldCheck4UpdateOnStartup()) Check4Update(); #endif @@ -543,5 +592,6 @@ namespace SourceGit private ResourceDictionary _activeLocale = null; private ResourceDictionary _themeOverrides = null; private ResourceDictionary _fontsOverrides = null; + private bool _createdSystemTrayIcon = false; } } diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index a24f1b65..48814c77 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -11,6 +11,7 @@ namespace SourceGit.Native [SupportedOSPlatform("linux")] internal class Linux : OS.IBackend { + private FileStream _fs = null; public void SetupApp(AppBuilder builder) { builder.With(new X11PlatformOptions() { EnableIme = true }); @@ -97,6 +98,26 @@ namespace SourceGit.Native } } + public bool EnsureSingleInstance() + { + var pidfile = Path.Combine(Path.GetTempPath(), "sourcegit.pid"); + var pid = Process.GetCurrentProcess().Id.ToString(); + Console.WriteLine("pid " + pid); + + try + { + _fs = File.OpenWrite(pidfile); + _fs.Lock(0, 1000); + new StreamWriter(_fs).Write(pid); + return true; + } + catch (IOException) + { + Console.WriteLine("another SourceGit is running"); + return false; + } + } + private string FindExecutable(string filename) { var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index c9e6abad..a4453513 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -86,5 +86,7 @@ namespace SourceGit.Native { Process.Start("open", $"\"{file}\""); } + + public bool EnsureSingleInstance() { return true; } } } diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 177bbf9f..d7c7785a 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -23,6 +23,8 @@ namespace SourceGit.Native void OpenInFileManager(string path, bool select); void OpenBrowser(string url); void OpenWithDefaultEditor(string file); + + bool EnsureSingleInstance(); } public static string DataDir { @@ -217,6 +219,11 @@ namespace SourceGit.Native [GeneratedRegex(@"^git version[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")] private static partial Regex REG_GIT_VERSION(); + public static bool EnsureSingleInstance() + { + return _backend.EnsureSingleInstance(); + } + private static IBackend _backend = null; private static string _gitExecutable = string.Empty; } diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 10f2970a..5302118e 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -14,6 +14,8 @@ namespace SourceGit.Native [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { + private FileStream _fs = null; + [StructLayout(LayoutKind.Sequential)] internal struct RTL_OSVERSIONINFOEX { @@ -393,5 +395,25 @@ namespace SourceGit.Native return null; } + + public bool EnsureSingleInstance() + { + var pidfile = Path.Combine(Path.GetTempPath(), "sourcegit.pid"); + var pid = Process.GetCurrentProcess().Id.ToString(); + Console.WriteLine("pid " + pid); + + try + { + _fs = File.OpenWrite(pidfile); + _fs.Lock(0, 1000); + new StreamWriter(_fs).Write(pid); + return true; + } + catch (IOException) + { + Console.WriteLine("another SourceGit is running"); + return false; + } + } } } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index a07de46b..a58e2a2f 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -458,6 +458,7 @@ Theme Overrides Use fixed tab width in titlebar Use native window frame + System tray icon (needs restart) DIFF/MERGE TOOL Install Path Input path for diff/merge tool diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 75650a28..67502530 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -462,6 +462,7 @@ Переопределение темы Использовать фиксированную ширину табуляции в строке заголовка. Использовать системное окно + Иконка в системном лотке (нужен перезапуск) ИНСТРУМЕНТ РАЗЛИЧИЙ/СЛИЯНИЯ Путь установки Введите путь для инструмента различия/слияния diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 6761eccf..f38d4b88 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -29,6 +29,8 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _activeWorkspace, value); } + public bool InterceptQuit { get; set; } = false; + public LauncherPage ActivePage { get => _activePage; @@ -47,7 +49,6 @@ namespace SourceGit.ViewModels public Launcher(string startupRepo) { _ignoreIndexChange = true; - Pages = new AvaloniaList(); AddNewTab(); diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 5cd7c8a4..c0b4d17c 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -348,6 +348,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _lastCheckUpdateTime, value); } + public bool SystemTrayIcon + { + get => _systemTrayIcon; + set => SetProperty(ref _systemTrayIcon, value); + } + public bool IsGitConfigured() { var path = GitInstallPath; @@ -682,5 +688,7 @@ namespace SourceGit.ViewModels private string _externalMergeToolPath = string.Empty; private uint _statisticsSampleColor = 0xFF00FF00; + + private bool _systemTrayIcon = false; } } diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 832cca80..ef955cd4 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -261,7 +261,13 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - (DataContext as ViewModels.Launcher)?.Quit(Width, Height); + var launcher = DataContext as ViewModels.Launcher; + if (launcher is { InterceptQuit: true }) { + e.Cancel = true; + Hide(); + } else { + launcher?.Quit(Width, Height); + } base.OnClosing(e); } diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 7f3633ad..b64e91e2 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -148,7 +148,7 @@ - + - + + - + - + + + + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 2bc5c571..e35bb2f7 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -333,6 +333,17 @@ namespace SourceGit.Views e.Handled = true; } + private void OnSystemTrayIconCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is CheckBox box) + { + ViewModels.Preferences.Instance.SystemTrayIcon = box.IsChecked == true; + var dialog = new ConfirmRestart(); + App.OpenDialog(dialog); + } + + e.Handled = true; + } private void OnGitInstallPathChanged(object sender, TextChangedEventArgs e) {