Skip to content

Commit e5f6aca

Browse files
committed
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
1 parent 632222a commit e5f6aca

File tree

13 files changed

+157
-21
lines changed

13 files changed

+157
-21
lines changed

src/App.Commands.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public static bool IsCheckForUpdateCommandVisible
3636
#endif
3737
}
3838
}
39-
39+
40+
public static readonly Command Unminimize = new Command(_ => ShowWindow());
4041
public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
4142
public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
4243
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));

src/App.axaml.cs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using Avalonia.Markup.Xaml;
1515
using Avalonia.Media;
1616
using Avalonia.Media.Fonts;
17+
using Avalonia.Media.Imaging;
18+
using Avalonia.Platform;
1719
using Avalonia.Platform.Storage;
1820
using Avalonia.Styling;
1921
using Avalonia.Threading;
@@ -167,6 +169,46 @@ public static void SetTheme(string theme, string themeOverridesFile)
167169
}
168170
}
169171

172+
public void SetupTrayIcon(bool enable)
173+
{
174+
if (enable && Native.OS.EnsureSingleInstance())
175+
{
176+
var icons = new TrayIcons {
177+
new TrayIcon {
178+
Icon = new WindowIcon(new Bitmap(AssetLoader.Open(new Uri("avares://SourceGit/App.ico")))),
179+
Menu = [
180+
new NativeMenuItem(Text("Open")) {Command = Unminimize},
181+
new NativeMenuItem(Text("Preferences")) {Command = OpenPreferencesCommand},
182+
new NativeMenuItemSeparator(),
183+
new NativeMenuItem(Text("Quit")) {Command = QuitCommand},
184+
]
185+
}
186+
};
187+
icons[0].Clicked += (_, _) => ToggleWindow();
188+
TrayIcon.SetIcons(Current, icons);
189+
_createdSystemTrayIcon = true;
190+
}
191+
}
192+
193+
private static void ToggleWindow() {
194+
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
195+
if (desktop.MainWindow.IsVisible) {
196+
desktop.MainWindow.Hide();
197+
} else {
198+
ShowWindow();
199+
}
200+
}
201+
}
202+
203+
private static void ShowWindow()
204+
{
205+
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
206+
desktop.MainWindow.WindowState = WindowState.Normal;
207+
desktop.MainWindow.Show();
208+
desktop.MainWindow.BringIntoView();
209+
desktop.MainWindow.Focus();
210+
}
211+
}
170212
public static void SetFonts(string defaultFont, string monospaceFont, bool onlyUseMonospaceFontInEditor)
171213
{
172214
var app = Current as App;
@@ -320,6 +362,7 @@ public override void OnFrameworkInitializationCompleted()
320362

321363
TryLaunchAsNormal(desktop);
322364
}
365+
base.OnFrameworkInitializationCompleted();
323366
}
324367
#endregion
325368

@@ -476,11 +519,17 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
476519
if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]))
477520
startupRepo = desktop.Args[0];
478521

479-
_launcher = new ViewModels.Launcher(startupRepo);
522+
var pref = ViewModels.Preferences.Instance;
523+
524+
SetupTrayIcon(pref.SystemTrayIcon);
525+
if (_createdSystemTrayIcon) {
526+
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
527+
}
528+
529+
_launcher = new ViewModels.Launcher(startupRepo) { InterceptQuit = _createdSystemTrayIcon };
480530
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
481531

482532
#if !DISABLE_UPDATE_DETECTION
483-
var pref = ViewModels.Preferences.Instance;
484533
if (pref.ShouldCheck4UpdateOnStartup())
485534
Check4Update();
486535
#endif
@@ -543,5 +592,6 @@ private void ShowSelfUpdateResult(object data)
543592
private ResourceDictionary _activeLocale = null;
544593
private ResourceDictionary _themeOverrides = null;
545594
private ResourceDictionary _fontsOverrides = null;
595+
private bool _createdSystemTrayIcon = false;
546596
}
547597
}

src/Native/Linux.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace SourceGit.Native
1111
[SupportedOSPlatform("linux")]
1212
internal class Linux : OS.IBackend
1313
{
14+
private FileStream _fs = null;
1415
public void SetupApp(AppBuilder builder)
1516
{
1617
builder.With(new X11PlatformOptions() { EnableIme = true });
@@ -97,6 +98,26 @@ public void OpenWithDefaultEditor(string file)
9798
}
9899
}
99100

101+
public bool EnsureSingleInstance()
102+
{
103+
var pidfile = Path.Combine(Path.GetTempPath(), "sourcegit.pid");
104+
var pid = Process.GetCurrentProcess().Id.ToString();
105+
Console.WriteLine("pid " + pid);
106+
107+
try
108+
{
109+
_fs = File.OpenWrite(pidfile);
110+
_fs.Lock(0, 1000);
111+
new StreamWriter(_fs).Write(pid);
112+
return true;
113+
}
114+
catch (IOException)
115+
{
116+
Console.WriteLine("another SourceGit is running");
117+
return false;
118+
}
119+
}
120+
100121
private string FindExecutable(string filename)
101122
{
102123
var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;

src/Native/MacOS.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,7 @@ public void OpenWithDefaultEditor(string file)
8686
{
8787
Process.Start("open", $"\"{file}\"");
8888
}
89+
90+
public bool EnsureSingleInstance() { return true; }
8991
}
9092
}

src/Native/OS.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public interface IBackend
2323
void OpenInFileManager(string path, bool select);
2424
void OpenBrowser(string url);
2525
void OpenWithDefaultEditor(string file);
26+
27+
bool EnsureSingleInstance();
2628
}
2729

2830
public static string DataDir {
@@ -217,6 +219,11 @@ private static void UpdateGitVersion()
217219
[GeneratedRegex(@"^git version[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")]
218220
private static partial Regex REG_GIT_VERSION();
219221

222+
public static bool EnsureSingleInstance()
223+
{
224+
return _backend.EnsureSingleInstance();
225+
}
226+
220227
private static IBackend _backend = null;
221228
private static string _gitExecutable = string.Empty;
222229
}

src/Native/Windows.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace SourceGit.Native
1414
[SupportedOSPlatform("windows")]
1515
internal class Windows : OS.IBackend
1616
{
17+
private FileStream _fs = null;
18+
1719
[StructLayout(LayoutKind.Sequential)]
1820
internal struct RTL_OSVERSIONINFOEX
1921
{
@@ -393,5 +395,25 @@ private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth)
393395

394396
return null;
395397
}
398+
399+
public bool EnsureSingleInstance()
400+
{
401+
var pidfile = Path.Combine(Path.GetTempPath(), "sourcegit.pid");
402+
var pid = Process.GetCurrentProcess().Id.ToString();
403+
Console.WriteLine("pid " + pid);
404+
405+
try
406+
{
407+
_fs = File.OpenWrite(pidfile);
408+
_fs.Lock(0, 1000);
409+
new StreamWriter(_fs).Write(pid);
410+
return true;
411+
}
412+
catch (IOException)
413+
{
414+
Console.WriteLine("another SourceGit is running");
415+
return false;
416+
}
417+
}
396418
}
397419
}

src/Resources/Locales/en_US.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@
458458
<x:String x:Key="Text.Preferences.Appearance.ThemeOverrides" xml:space="preserve">Theme Overrides</x:String>
459459
<x:String x:Key="Text.Preferences.Appearance.UseFixedTabWidth" xml:space="preserve">Use fixed tab width in titlebar</x:String>
460460
<x:String x:Key="Text.Preferences.Appearance.UseNativeWindowFrame" xml:space="preserve">Use native window frame</x:String>
461+
<x:String x:Key="Text.Preferences.Appearance.SystemTrayIcon" xml:space="preserve">System tray icon (needs restart)</x:String>
461462
<x:String x:Key="Text.Preferences.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
462463
<x:String x:Key="Text.Preferences.DiffMerge.Path" xml:space="preserve">Install Path</x:String>
463464
<x:String x:Key="Text.Preferences.DiffMerge.Path.Placeholder" xml:space="preserve">Input path for diff/merge tool</x:String>

src/Resources/Locales/ru_RU.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@
462462
<x:String x:Key="Text.Preferences.Appearance.ThemeOverrides" xml:space="preserve">Переопределение темы</x:String>
463463
<x:String x:Key="Text.Preferences.Appearance.UseFixedTabWidth" xml:space="preserve">Использовать фиксированную ширину табуляции в строке заголовка.</x:String>
464464
<x:String x:Key="Text.Preferences.Appearance.UseNativeWindowFrame" xml:space="preserve">Использовать системное окно</x:String>
465+
<x:String x:Key="Text.Preferences.Appearance.SystemTrayIcon" xml:space="preserve">Иконка в системном лотке (нужен перезапуск)</x:String>
465466
<x:String x:Key="Text.Preferences.DiffMerge" xml:space="preserve">ИНСТРУМЕНТ РАЗЛИЧИЙ/СЛИЯНИЯ</x:String>
466467
<x:String x:Key="Text.Preferences.DiffMerge.Path" xml:space="preserve">Путь установки</x:String>
467468
<x:String x:Key="Text.Preferences.DiffMerge.Path.Placeholder" xml:space="preserve">Введите путь для инструмента различия/слияния</x:String>

src/ViewModels/Launcher.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public Workspace ActiveWorkspace
2929
private set => SetProperty(ref _activeWorkspace, value);
3030
}
3131

32+
public bool InterceptQuit { get; set; } = false;
33+
3234
public LauncherPage ActivePage
3335
{
3436
get => _activePage;
@@ -47,7 +49,6 @@ public LauncherPage ActivePage
4749
public Launcher(string startupRepo)
4850
{
4951
_ignoreIndexChange = true;
50-
5152
Pages = new AvaloniaList<LauncherPage>();
5253
AddNewTab();
5354

src/ViewModels/Preferences.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,12 @@ public double LastCheckUpdateTime
348348
set => SetProperty(ref _lastCheckUpdateTime, value);
349349
}
350350

351+
public bool SystemTrayIcon
352+
{
353+
get => _systemTrayIcon;
354+
set => SetProperty(ref _systemTrayIcon, value);
355+
}
356+
351357
public bool IsGitConfigured()
352358
{
353359
var path = GitInstallPath;
@@ -682,5 +688,7 @@ private string FixFontFamilyName(string name)
682688
private string _externalMergeToolPath = string.Empty;
683689

684690
private uint _statisticsSampleColor = 0xFF00FF00;
691+
692+
private bool _systemTrayIcon = false;
685693
}
686694
}

src/Views/Launcher.axaml.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,13 @@ protected override void OnKeyUp(KeyEventArgs e)
261261

262262
protected override void OnClosing(WindowClosingEventArgs e)
263263
{
264-
(DataContext as ViewModels.Launcher)?.Quit(Width, Height);
264+
var launcher = DataContext as ViewModels.Launcher;
265+
if (launcher is { InterceptQuit: true }) {
266+
e.Cancel = true;
267+
Hide();
268+
} else {
269+
launcher?.Quit(Width, Height);
270+
}
265271
base.OnClosing(e);
266272
}
267273

src/Views/Preferences.axaml

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
<TabItem.Header>
149149
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.Appearance}"/>
150150
</TabItem.Header>
151-
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
151+
<Grid Margin="8" RowDefinitions="32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
152152
<TextBlock Grid.Row="0" Grid.Column="0"
153153
Text="{DynamicResource Text.Preferences.Appearance.Theme}"
154154
HorizontalAlignment="Right"
@@ -232,21 +232,26 @@
232232
</TextBox.InnerRightContent>
233233
</TextBox>
234234

235-
<CheckBox Grid.Row="5" Grid.Column="1"
236-
Content="{DynamicResource Text.Preferences.Appearance.OnlyUseMonoFontInEditor}"
237-
IsChecked="{Binding OnlyUseMonoFontInEditor, Mode=TwoWay}"/>
238-
239-
<CheckBox Grid.Row="6" Grid.Column="1"
240-
Height="32"
241-
Content="{DynamicResource Text.Preferences.Appearance.UseFixedTabWidth}"
242-
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseFixedTabWidth, Mode=TwoWay}"/>
243-
244-
<CheckBox Grid.Row="7" Grid.Column="1"
245-
Height="32"
246-
Content="{DynamicResource Text.Preferences.Appearance.UseNativeWindowFrame}"
247-
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSystemWindowFrame, Mode=OneTime}"
248-
IsVisible="{OnPlatform False, Linux=True}"
249-
IsCheckedChanged="OnUseNativeWindowFrameChanged"/>
235+
<StackPanel Grid.Row="5" Grid.Column="1">
236+
<CheckBox Content="{DynamicResource Text.Preferences.Appearance.OnlyUseMonoFontInEditor}"
237+
IsChecked="{Binding OnlyUseMonoFontInEditor, Mode=TwoWay}"/>
238+
239+
<CheckBox Height="32"
240+
Content="{DynamicResource Text.Preferences.Appearance.UseFixedTabWidth}"
241+
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseFixedTabWidth, Mode=TwoWay}"/>
242+
243+
<CheckBox Height="32"
244+
Content="{DynamicResource Text.Preferences.Appearance.UseNativeWindowFrame}"
245+
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSystemWindowFrame, Mode=OneTime}"
246+
IsVisible="{OnPlatform False, Linux=True}"
247+
IsCheckedChanged="OnUseNativeWindowFrameChanged"/>
248+
249+
<CheckBox Height="32"
250+
Content="{DynamicResource Text.Preferences.Appearance.SystemTrayIcon}"
251+
IsChecked="{Binding Path=SystemTrayIcon, Mode=OneTime}"
252+
IsVisible="True"
253+
IsCheckedChanged="OnSystemTrayIconCheckedChanged"/>
254+
</StackPanel>
250255
</Grid>
251256
</TabItem>
252257

src/Views/Preferences.axaml.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,17 @@ private void OnUseNativeWindowFrameChanged(object sender, RoutedEventArgs e)
333333

334334
e.Handled = true;
335335
}
336+
private void OnSystemTrayIconCheckedChanged(object sender, RoutedEventArgs e)
337+
{
338+
if (sender is CheckBox box)
339+
{
340+
ViewModels.Preferences.Instance.SystemTrayIcon = box.IsChecked == true;
341+
var dialog = new ConfirmRestart();
342+
App.OpenDialog(dialog);
343+
}
344+
345+
e.Handled = true;
346+
}
336347

337348
private void OnGitInstallPathChanged(object sender, TextChangedEventArgs e)
338349
{

0 commit comments

Comments
 (0)