From ae7d576967989b82f3432f714a4c3c771e816096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Tue, 21 Oct 2025 15:40:19 +0200 Subject: [PATCH] Fixing system tray icon. --- .../Interfaces/ISystemTrayService.cs | 36 ---- .../Translations/en_US/resources.json | 2 + Wino.Mail.WinUI/App.xaml.cs | 22 +- Wino.Mail.WinUI/Services/SystemTrayService.cs | 196 ------------------ Wino.Mail.WinUI/ShellWindow.xaml | 18 ++ Wino.Mail.WinUI/ShellWindow.xaml.cs | 52 ++--- 6 files changed, 51 insertions(+), 275 deletions(-) delete mode 100644 Wino.Core.Domain/Interfaces/ISystemTrayService.cs delete mode 100644 Wino.Mail.WinUI/Services/SystemTrayService.cs diff --git a/Wino.Core.Domain/Interfaces/ISystemTrayService.cs b/Wino.Core.Domain/Interfaces/ISystemTrayService.cs deleted file mode 100644 index 7a67953c..00000000 --- a/Wino.Core.Domain/Interfaces/ISystemTrayService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Interfaces; - -public interface ISystemTrayService -{ - /// - /// Initializes the system tray icon. - /// - void Initialize(); - - /// - /// Shows the system tray icon. - /// - void Show(); - - /// - /// Hides the system tray icon. - /// - void Hide(); - - /// - /// Event fired when the tray icon is double-clicked. - /// - event EventHandler? TrayIconDoubleClicked; - - /// - /// Gets whether the tray icon is currently minimized. - /// - bool IsMinimizedToTray { get; } - - /// - /// Disposes of the system tray resources. - /// - void Dispose(); -} \ No newline at end of file diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index c84f20f3..b6464abb 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -686,6 +686,8 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Open Wino Mail", + "SystemTrayMenu_ExitWino": "Exit", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 19ce50b5..8fff4f55 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -2,13 +2,13 @@ using System.Text; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Windows.AppLifecycle; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.WinUI; using Wino.Core.WinUI.Interfaces; using Wino.Mail.Services; using Wino.Mail.ViewModels; -using Wino.Mail.WinUI.Services; using Wino.Messaging.Server; using Wino.Services; namespace Wino.Mail.WinUI; @@ -36,7 +36,6 @@ public partial class App : WinoApplication, IRecipient(); services.AddTransient(); services.AddSingleton(); - services.AddSingleton(); } private void RegisterViewModels(IServiceCollection services) @@ -79,6 +78,8 @@ public partial class App : WinoApplication, IRecipient AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask; + protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { // TODO: Check app relaunch mutex before loading anything. @@ -99,19 +100,16 @@ public partial class App : WinoApplication, IRecipient(); - if (systemTrayService != null) - { - systemTrayService.Initialize(); - systemTrayService.Show(); // Explicitly show the tray icon - } - if (MainWindow is not IWinoShellWindow shellWindow) throw new ArgumentException("MainWindow must implement IWinoShellWindow"); - shellWindow.HandleAppActivation(args); + bool isStartupTaskLaunch = IsStartupTaskLaunch(); - MainWindow.Activate(); + // Do not actiavate window if launched from startup task. Keep running in the system tray. + if (!isStartupTaskLaunch) + { + shellWindow.HandleAppActivation(args); + MainWindow.Activate(); + } } private void RegisterRecipients() diff --git a/Wino.Mail.WinUI/Services/SystemTrayService.cs b/Wino.Mail.WinUI/Services/SystemTrayService.cs deleted file mode 100644 index ca82ac8d..00000000 --- a/Wino.Mail.WinUI/Services/SystemTrayService.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Windows.Input; -using H.NotifyIcon; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media.Imaging; -using Wino.Core.Domain.Interfaces; -using Microsoft.Extensions.DependencyInjection; -using Wino.Core.WinUI; - -namespace Wino.Mail.WinUI.Services; - -public class SystemTrayService : ISystemTrayService -{ - private TaskbarIcon? _taskbarIcon; - private bool _isDisposed; - private bool _isMinimizedToTray; - - public bool IsMinimizedToTray => _isMinimizedToTray; - - public event EventHandler? TrayIconDoubleClicked; - - public void Initialize() - { - if (_taskbarIcon != null) return; - - try - { - System.Diagnostics.Debug.WriteLine("Starting system tray initialization..."); - - // Create TaskbarIcon first - _taskbarIcon = new TaskbarIcon(); - - // Set basic properties first - _taskbarIcon.ToolTipText = "Wino Mail"; - - // Configure the taskbar icon with icon loading - var iconUri = new Uri("ms-appx:///Assets/Wino_Icon.ico"); - var bitmapImage = new BitmapImage(iconUri); - _taskbarIcon.IconSource = bitmapImage; - System.Diagnostics.Debug.WriteLine("Icon source set"); - - // Create context menu - var contextMenu = new MenuFlyout(); - - // Show Window menu item - var showMenuItem = new MenuFlyoutItem - { - Text = "Show Wino Mail", - Icon = new SymbolIcon(Symbol.Home) - }; - showMenuItem.Click += ShowMenuItem_Click; - contextMenu.Items.Add(showMenuItem); - System.Diagnostics.Debug.WriteLine("Show menu item added"); - - // Separator - contextMenu.Items.Add(new MenuFlyoutSeparator()); - - // Exit menu item - var exitMenuItem = new MenuFlyoutItem - { - Text = "Exit", - Icon = new SymbolIcon(Symbol.Cancel) - }; - exitMenuItem.Click += ExitMenuItem_Click; - contextMenu.Items.Add(exitMenuItem); - System.Diagnostics.Debug.WriteLine("Exit menu item added"); - - // Set context menu - _taskbarIcon.ContextFlyout = contextMenu; - - // Handle double-click using the proper event - _taskbarIcon.LeftClickCommand = new RelayCommand(OnTrayIconLeftClick); - - // Set visibility and create explicitly - _taskbarIcon.Visibility = Visibility.Visible; - - // Try ForceCreate to ensure the icon is properly created in the system tray - _taskbarIcon.ForceCreate(); - System.Diagnostics.Debug.WriteLine("System tray icon created and visible"); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Failed to initialize system tray: {ex.Message}"); - System.Diagnostics.Debug.WriteLine($"Stack trace: {ex.StackTrace}"); - } - } - - private void ShowMenuItem_Click(object sender, RoutedEventArgs e) - { - System.Diagnostics.Debug.WriteLine("Show menu item clicked"); - TrayIconDoubleClicked?.Invoke(this, EventArgs.Empty); - } - - private void ExitMenuItem_Click(object sender, RoutedEventArgs e) - { - System.Diagnostics.Debug.WriteLine("Exit menu item clicked"); - ExitApplication(); - } - - private void OnTrayIconLeftClick() - { - System.Diagnostics.Debug.WriteLine("Tray icon left clicked"); - TrayIconDoubleClicked?.Invoke(this, EventArgs.Empty); - } - - public void Show() - { - if (_taskbarIcon != null) - { - try - { - _taskbarIcon.Visibility = Visibility.Visible; - _taskbarIcon.ForceCreate(); // Ensure the icon is properly created and visible - _isMinimizedToTray = true; - System.Diagnostics.Debug.WriteLine("System tray icon set to visible and force created"); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Failed to show system tray icon: {ex.Message}"); - } - } - else - { - System.Diagnostics.Debug.WriteLine("TaskbarIcon is null when trying to show"); - } - } - - public void Hide() - { - if (_taskbarIcon != null) - { - _taskbarIcon.Visibility = Visibility.Collapsed; - _isMinimizedToTray = false; - } - } - - private void ExitApplication() - { - System.Diagnostics.Debug.WriteLine("Attempting to exit application..."); - - try - { - // Clean up the tray icon first - Dispose(); - - // Get the main window and close it properly - if (WinoApplication.MainWindow is ShellWindow shellWindow) - { - // Force close the window without minimizing to tray - shellWindow.ForceClose(); - } - else - { - // Fallback to application exit - Application.Current.Exit(); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Error during application exit: {ex.Message}"); - // Force exit if normal exit fails - Environment.Exit(0); - } - } - - public void Dispose() - { - if (_isDisposed) return; - - _taskbarIcon?.Dispose(); - _taskbarIcon = null; - _isDisposed = true; - } -} - -// Simple RelayCommand implementation for the tray icon -public class RelayCommand : ICommand -{ - private readonly Action _execute; - private readonly Func? _canExecute; - - public RelayCommand(Action execute, Func? canExecute = null) - { - _execute = execute ?? throw new ArgumentNullException(nameof(execute)); - _canExecute = canExecute; - } - - public event EventHandler? CanExecuteChanged; - - public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true; - - public void Execute(object? parameter) => _execute(); - - public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); -} diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml index c2f6c1ad..4819459d 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml +++ b/Wino.Mail.WinUI/ShellWindow.xaml @@ -4,8 +4,10 @@ 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:domain="using:Wino.Core.Domain" xmlns:local="using:Wino.Mail.WinUI" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:notifyicon="using:H.NotifyIcon" xmlns:winuiex="using:WinUIEx" Title="ShellWindow" mc:Ignorable="d"> @@ -30,9 +32,25 @@ IsBackButtonVisible="{x:Bind StatePersistanceService.IsBackButtonVisible, Mode=OneWay}" IsPaneToggleButtonVisible="True" PaneToggleRequested="PaneButtonClicked" /> + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/ShellWindow.xaml.cs b/Wino.Mail.WinUI/ShellWindow.xaml.cs index 99bbf187..1f42f70d 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml.cs +++ b/Wino.Mail.WinUI/ShellWindow.xaml.cs @@ -1,4 +1,6 @@ using System; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; @@ -19,7 +21,9 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient { public IStatePersistanceService StatePersistanceService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception("StatePersistanceService not registered in DI container."); public IPreferencesService PreferencesService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception("PreferencesService not registered in DI container."); - private readonly ISystemTrayService _systemTrayService; + + public ICommand ShowWinoCommand { get; set; } + public ICommand ExitWinoCommand { get; set; } public ShellWindow() { @@ -31,22 +35,22 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient MinHeight = 420; ConfigureTitleBar(); - // Initialize system tray service - _systemTrayService = WinoApplication.Current.Services.GetService() ?? throw new Exception("SystemTrayService not registered in DI container."); - _systemTrayService.Initialize(); - _systemTrayService.TrayIconDoubleClicked += OnTrayIconDoubleClicked; - // Handle window closing event to minimize to tray instead of closing Closed += OnWindowClosed; // Use the AppWindow.Closing event to handle the close request AppWindow.Closing += OnAppWindowClosing; + + ShowWinoCommand = new RelayCommand(RestoreFromTray); + ExitWinoCommand = new RelayCommand(ForceClose); + + SystemTrayIcon.ForceCreate(); } private void ConfigureTitleBar() { AppWindow.TitleBar.ExtendsContentIntoTitleBar = true; - + // Apply initial theme colors var themeService = WinoApplication.Current.Services.GetService(); if (themeService != null) @@ -133,54 +137,40 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient private void OnAppWindowClosing(object sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs e) { - // Cancel the close and minimize to tray instead e.Cancel = true; MinimizeToTray(); } private void OnWindowClosed(object sender, WindowEventArgs e) { - // Clean up tray icon when window is actually closed - _systemTrayService?.Dispose(); + SystemTrayIcon?.Dispose(); } private void MinimizeToTray() { - // Hide the window and show tray icon this.Hide(); - _systemTrayService.Show(); - } - - private void OnTrayIconDoubleClicked(object? sender, EventArgs e) - { - // Restore the window from tray - RestoreFromTray(); + SystemTrayIcon.ForceCreate(); } private void RestoreFromTray() { - if (_systemTrayService.IsMinimizedToTray) - { - // Show the window and hide tray icon - this.Show(); - this.Activate(); - _systemTrayService.Hide(); - } + this.Show(); + BringToFront(); } public void ForceClose() { // Unsubscribe from the closing event to avoid infinite loop AppWindow.Closing -= OnAppWindowClosing; - + // Clean up system tray - _systemTrayService?.Dispose(); - + SystemTrayIcon?.Dispose(); + UnregisterRecipients(); - + // Close the window - this.Close(); - + Close(); + // Exit the application Application.Current.Exit(); }