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)
{