Skip to content

Commit

Permalink
System tray icon
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wl2776 committed Jan 13, 2025
1 parent 458a1ea commit beef6b6
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/App.Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public static bool IsCheckForUpdateCommandVisible
#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));
Expand Down
54 changes: 52 additions & 2 deletions src/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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;
Expand Down Expand Up @@ -167,6 +169,46 @@ public static void SetTheme(string theme, string themeOverridesFile)
}
}

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;
Expand Down Expand Up @@ -320,6 +362,7 @@ public override void OnFrameworkInitializationCompleted()

TryLaunchAsNormal(desktop);
}
base.OnFrameworkInitializationCompleted();
}
#endregion

Expand Down Expand Up @@ -476,11 +519,17 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
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
Expand Down Expand Up @@ -543,5 +592,6 @@ private void ShowSelfUpdateResult(object data)
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;
private ResourceDictionary _fontsOverrides = null;
private bool _createdSystemTrayIcon = false;
}
}
21 changes: 21 additions & 0 deletions src/Native/Linux.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -97,6 +98,26 @@ public void OpenWithDefaultEditor(string file)
}
}

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;
Expand Down
2 changes: 2 additions & 0 deletions src/Native/MacOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,7 @@ public void OpenWithDefaultEditor(string file)
{
Process.Start("open", $"\"{file}\"");
}

public bool EnsureSingleInstance() { return true; }
}
}
7 changes: 7 additions & 0 deletions src/Native/OS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public interface IBackend
void OpenInFileManager(string path, bool select);
void OpenBrowser(string url);
void OpenWithDefaultEditor(string file);

bool EnsureSingleInstance();
}

public static string DataDir {
Expand Down Expand Up @@ -217,6 +219,11 @@ private static void UpdateGitVersion()
[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;
}
Expand Down
22 changes: 22 additions & 0 deletions src/Native/Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -393,5 +395,25 @@ private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth)

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;
}
}
}
}
1 change: 1 addition & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
<x:String x:Key="Text.Preferences.Appearance.ThemeOverrides" xml:space="preserve">Theme Overrides</x:String>
<x:String x:Key="Text.Preferences.Appearance.UseFixedTabWidth" xml:space="preserve">Use fixed tab width in titlebar</x:String>
<x:String x:Key="Text.Preferences.Appearance.UseNativeWindowFrame" xml:space="preserve">Use native window frame</x:String>
<x:String x:Key="Text.Preferences.Appearance.SystemTrayIcon" xml:space="preserve">System tray icon (needs restart)</x:String>
<x:String x:Key="Text.Preferences.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.Path.Placeholder" xml:space="preserve">Input path for diff/merge tool</x:String>
Expand Down
1 change: 1 addition & 0 deletions src/Resources/Locales/ru_RU.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@
<x:String x:Key="Text.Preferences.Appearance.ThemeOverrides" xml:space="preserve">Переопределение темы</x:String>
<x:String x:Key="Text.Preferences.Appearance.UseFixedTabWidth" xml:space="preserve">Использовать фиксированную ширину табуляции в строке заголовка.</x:String>
<x:String x:Key="Text.Preferences.Appearance.UseNativeWindowFrame" xml:space="preserve">Использовать системное окно</x:String>
<x:String x:Key="Text.Preferences.Appearance.SystemTrayIcon" xml:space="preserve">Иконка в системном лотке (нужен перезапуск)</x:String>
<x:String x:Key="Text.Preferences.DiffMerge" xml:space="preserve">ИНСТРУМЕНТ РАЗЛИЧИЙ/СЛИЯНИЯ</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.Path" xml:space="preserve">Путь установки</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.Path.Placeholder" xml:space="preserve">Введите путь для инструмента различия/слияния</x:String>
Expand Down
3 changes: 2 additions & 1 deletion src/ViewModels/Launcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public Workspace ActiveWorkspace
private set => SetProperty(ref _activeWorkspace, value);
}

public bool InterceptQuit { get; set; } = false;

public LauncherPage ActivePage
{
get => _activePage;
Expand All @@ -47,7 +49,6 @@ public LauncherPage ActivePage
public Launcher(string startupRepo)
{
_ignoreIndexChange = true;

Pages = new AvaloniaList<LauncherPage>();
AddNewTab();

Expand Down
8 changes: 8 additions & 0 deletions src/ViewModels/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,12 @@ public double LastCheckUpdateTime
set => SetProperty(ref _lastCheckUpdateTime, value);
}

public bool SystemTrayIcon
{
get => _systemTrayIcon;
set => SetProperty(ref _systemTrayIcon, value);
}

public bool IsGitConfigured()
{
var path = GitInstallPath;
Expand Down Expand Up @@ -682,5 +688,7 @@ private string FixFontFamilyName(string name)
private string _externalMergeToolPath = string.Empty;

private uint _statisticsSampleColor = 0xFF00FF00;

private bool _systemTrayIcon = false;
}
}
8 changes: 7 additions & 1 deletion src/Views/Launcher.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,13 @@ protected override void OnKeyUp(KeyEventArgs e)

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

Expand Down
9 changes: 8 additions & 1 deletion src/Views/Preferences.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.Appearance}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preferences.Appearance.Theme}"
HorizontalAlignment="Right"
Expand Down Expand Up @@ -247,6 +247,13 @@
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSystemWindowFrame, Mode=OneTime}"
IsVisible="{OnPlatform False, Linux=True}"
IsCheckedChanged="OnUseNativeWindowFrameChanged"/>

<CheckBox Grid.Row="{OnPlatform 7, Linux=8}" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preference.Appearance.SystemTrayIcon}"
IsChecked="{Binding Path=SystemTrayIcon, Mode=OneTime}"
IsVisible="True"
IsCheckedChanged="OnSystemTrayIconCheckedChanged"/>
</Grid>
</TabItem>

Expand Down
11 changes: 11 additions & 0 deletions src/Views/Preferences.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,17 @@ private void OnUseNativeWindowFrameChanged(object sender, RoutedEventArgs e)

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

0 comments on commit beef6b6

Please sign in to comment.