diff --git a/Directory.Packages.props b/Directory.Packages.props index b800ef26..710751ef 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,6 +51,7 @@ + diff --git a/Wino.Core.Domain/Interfaces/ISystemTrayService.cs b/Wino.Core.Domain/Interfaces/ISystemTrayService.cs new file mode 100644 index 00000000..7a67953c --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ISystemTrayService.cs @@ -0,0 +1,36 @@ +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.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 268e044b..a2d97c06 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -8,6 +8,7 @@ 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; @@ -33,6 +34,7 @@ public partial class App : WinoApplication, IRecipient(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); } private void RegisterViewModels(IServiceCollection services) @@ -91,6 +93,14 @@ 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); diff --git a/Wino.Mail.WinUI/Assets/Wino_Icon.ico b/Wino.Mail.WinUI/Assets/Wino_Icon.ico new file mode 100644 index 00000000..be12c893 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/Wino_Icon.ico differ diff --git a/Wino.Mail.WinUI/Services/SystemTrayService.cs b/Wino.Mail.WinUI/Services/SystemTrayService.cs new file mode 100644 index 00000000..ca82ac8d --- /dev/null +++ b/Wino.Mail.WinUI/Services/SystemTrayService.cs @@ -0,0 +1,196 @@ +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.cs b/Wino.Mail.WinUI/ShellWindow.xaml.cs index c03fa2e7..efb8c768 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml.cs +++ b/Wino.Mail.WinUI/ShellWindow.xaml.cs @@ -19,6 +19,7 @@ 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 ShellWindow() { @@ -30,6 +31,17 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient MinWidth = 420; 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; } private void ConfigureTitleBar() @@ -119,4 +131,56 @@ 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(); + } + + 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(); + } + + private void RestoreFromTray() + { + if (_systemTrayService.IsMinimizedToTray) + { + // Show the window and hide tray icon + this.Show(); + this.Activate(); + _systemTrayService.Hide(); + } + } + + public void ForceClose() + { + // Unsubscribe from the closing event to avoid infinite loop + AppWindow.Closing -= OnAppWindowClosing; + + // Clean up system tray + _systemTrayService?.Dispose(); + + // Close the window + this.Close(); + + // Exit the application + Application.Current.Exit(); + } } diff --git a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj index 1b1f2a4f..666cec74 100644 --- a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj +++ b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj @@ -72,6 +72,7 @@ + @@ -112,6 +113,7 @@ +