diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 4c0f325a..08f0d2ba 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -1041,8 +1041,9 @@ "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_ShowWinoCalendar": "Open Wino Calendar", + "SystemTrayMenu_Open": "Open", + "SystemTrayMenu_ShowWino": "Open Mail", + "SystemTrayMenu_ShowWinoCalendar": "Open Calendar", "SystemTrayMenu_ExitWino": "Exit", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 8fc53701..bc369bc3 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System; using System.IO; using System.Linq; using System.Text; @@ -8,6 +8,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Toolkit.Uwp.Notifications; +using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppNotifications; @@ -57,9 +58,14 @@ public partial class App : WinoApplication, private IPreferencesService? _preferencesService; private IAccountService? _accountService; private bool _windowManagerConfigured; + private bool _hasConfiguredAccounts; + private bool _isExiting; private CancellationTokenSource? _autoSynchronizationLoopCts; private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1); private readonly Dictionary _inboxSyncCounters = []; + private NativeTrayIcon? _trayIcon; + + internal bool IsExiting => _isExiting; public App() { @@ -117,6 +123,142 @@ public partial class App : WinoApplication, InitializeNavigationDispatcher(); } + private void EnsureTrayIconCreated() + { + if (_trayIcon != null) + return; + + var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Wino_Icon.ico"); + var dispatcherQueue = DispatcherQueue.GetForCurrentThread() + ?? throw new InvalidOperationException("Tray icon must be created on a thread with a DispatcherQueue."); + + _trayIcon = new NativeTrayIcon( + dispatcherQueue, + iconPath, + "Wino Mail", + BuildTrayMenu, + ActivatePreferredWindowAsync); + + _trayIcon.Create(); + } + + private IReadOnlyList BuildTrayMenu() + { + List items = + [ + new(Translator.SystemTrayMenu_Open, ActivatePreferredWindowAsync, IsDefault: true), + new(Translator.SystemTrayMenu_ShowWino, OpenMailFromTrayAsync) + ]; + + items.Add(new NativeTrayIcon.NativeTrayMenuItem( + Translator.SystemTrayMenu_ShowWinoCalendar, + OpenCalendarFromTrayAsync)); + items.Add(new NativeTrayIcon.NativeTrayMenuItem( + Translator.SystemTrayMenu_ExitWino, + ExitApplicationAsync)); + + return items; + } + + private Task ActivatePreferredWindowAsync() + { + if (!_hasConfiguredAccounts) + return ActivateWelcomeWindowAsync(); + + return ActivateShellWindowAsync(_preferencesService?.DefaultApplicationMode); + } + + private Task OpenMailFromTrayAsync() + => _hasConfiguredAccounts + ? ActivateShellWindowAsync(WinoApplicationMode.Mail) + : ActivateWelcomeWindowAsync(); + + private Task OpenCalendarFromTrayAsync() + => _hasConfiguredAccounts + ? ActivateShellWindowAsync(WinoApplicationMode.Calendar) + : ActivateWelcomeWindowAsync(); + + private async Task ActivateWelcomeWindowAsync() + { + var windowManager = Services.GetRequiredService(); + var welcomeWindow = windowManager.GetWindow(WinoWindowKind.Welcome) as WelcomeWindow; + + if (welcomeWindow == null) + { + CreateWelcomeWindow(); + welcomeWindow = MainWindow as WelcomeWindow; + } + + if (welcomeWindow == null) + return; + + windowManager.HideWindow(WinoWindowKind.Shell); + await ActivateWindowAsync(welcomeWindow); + } + + private async Task ActivateShellWindowAsync(WinoApplicationMode? mode, IWinoShellWindow? existingShellWindow = null) + { + var windowManager = Services.GetRequiredService(); + var shellWindow = existingShellWindow; + + if (shellWindow == null) + { + shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow; + + if (shellWindow == null) + { + CreateWindow(null); + shellWindow = MainWindow as IWinoShellWindow; + } + } + + if (shellWindow == null) + return; + + if (mode.HasValue) + shellWindow.HandleAppActivation(GetModeLaunchArgument(mode.Value)); + + CloseWelcomeWindowIfPresent(); + await ActivateWindowAsync((WindowEx)shellWindow); + } + + private void CloseWelcomeWindowIfPresent() + { + var windowManager = Services.GetRequiredService(); + if (windowManager.GetWindow(WinoWindowKind.Welcome) is not WelcomeWindow welcomeWindow) + return; + + welcomeWindow.AllowClose(); + welcomeWindow.Close(); + } + + private async Task ActivateWindowAsync(WindowEx window) + { + var windowManager = Services.GetRequiredService(); + MainWindow = window; + windowManager.ActivateWindow(window); + await NewThemeService.ApplyThemeToActiveWindowAsync(); + } + + private Task ExitApplicationAsync() + { + ExitApplication(); + return Task.CompletedTask; + } + + private void ExitApplication() + { + if (_isExiting) + return; + + _isExiting = true; + _trayIcon?.Dispose(); + _trayIcon = null; + + Services.GetRequiredService().CloseAllWindows(); + Application.Current.Exit(); + } + public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args) { var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); @@ -228,8 +370,10 @@ public partial class App : WinoApplication, _accountService = Services.GetRequiredService(); EnsureWindowManagerConfigured(); + EnsureTrayIconCreated(); var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any(); + _hasConfiguredAccounts = hasAnyAccount; if (!IsStartupTaskLaunch() && !hasAnyAccount) { CreateWelcomeWindow(); @@ -254,16 +398,25 @@ public partial class App : WinoApplication, // Check if launched by startup task. bool isStartupTaskLaunch = IsStartupTaskLaunch(); - // Create the window (needed for system tray icon even in startup task scenario). - CreateWindow(args); + if (isStartupTaskLaunch && !hasAnyAccount) + { + CreateWelcomeWindow(); + } + else + { + CreateWindow(args); + } // Initialize theme service after window creation. // Theme service requires the window to exist to properly load and apply themes. await NewThemeService.InitializeAsync(); - // Wino account loading and activation. - await LoadInitialWinoAccountAsync(); - await HandlePostActivationAsync(AppInstance.GetCurrent().GetActivatedEventArgs()); + if (hasAnyAccount) + { + // Wino account loading and activation. + await LoadInitialWinoAccountAsync(); + await HandlePostActivationAsync(AppInstance.GetCurrent().GetActivatedEventArgs()); + } LogActivation("Theme service initialized."); @@ -477,7 +630,7 @@ public partial class App : WinoApplication, if (mailItem == null) { LogActivation("Mail item not found. Exiting."); - Application.Current.Exit(); + ExitApplication(); return; } @@ -504,7 +657,7 @@ public partial class App : WinoApplication, if (_synchronizationManager == null) { LogActivation("Synchronization manager is not initialized. Exiting."); - Application.Current.Exit(); + ExitApplication(); return; } @@ -550,7 +703,7 @@ public partial class App : WinoApplication, LogActivation("Toast action handling complete. Exiting app."); // Exit the app after synchronization is complete. - Application.Current.Exit(); + ExitApplication(); } } @@ -731,6 +884,8 @@ public partial class App : WinoApplication, public void Receive(AccountCreatedMessage message) { + _hasConfiguredAccounts = true; + var windowManager = Services.GetRequiredService(); var navigationService = Services.GetRequiredService(); @@ -742,10 +897,11 @@ public partial class App : WinoApplication, { // Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher. CreateWindow(null); - windowManager.HideWindow(WinoWindowKind.Welcome); + CloseWelcomeWindowIfPresent(); navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); - await NewThemeService.ApplyThemeToActiveWindowAsync(); - MainWindow?.Activate(); + if (MainWindow != null) + await ActivateWindowAsync(MainWindow); + RestartAutoSynchronizationLoop(); }); } @@ -761,15 +917,16 @@ public partial class App : WinoApplication, MainWindow?.DispatcherQueue?.TryEnqueue(async () => { var accounts = await _accountService!.GetAccountsAsync(); - if (accounts.Any()) return; + _hasConfiguredAccounts = accounts.Any(); + if (_hasConfiguredAccounts) return; // All accounts removed — go back to welcome wizard from step 1 Services.GetRequiredService().Reset(); StopAutoSynchronizationLoop(); CreateWelcomeWindow(); windowManager.HideWindow(WinoWindowKind.Shell); - await NewThemeService.ApplyThemeToActiveWindowAsync(); - MainWindow?.Activate(); + if (MainWindow != null) + await ActivateWindowAsync(MainWindow); }); } diff --git a/Wino.Mail.WinUI/Services/NativeTrayIcon.cs b/Wino.Mail.WinUI/Services/NativeTrayIcon.cs new file mode 100644 index 00000000..7b1da82c --- /dev/null +++ b/Wino.Mail.WinUI/Services/NativeTrayIcon.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.UI.Dispatching; +using Serilog; + +namespace Wino.Mail.WinUI.Services; + +internal sealed class NativeTrayIcon : IDisposable +{ + private const int ImageIcon = 1; + private const int LoadFromFile = 0x0010; + private const int WmApp = 0x8000; + private const int WmNull = 0x0000; + private const int WmDestroy = 0x0002; + private const int WmClose = 0x0010; + private const int WmCommand = 0x0111; + private const int WmRButtonUp = 0x0205; + private const int WmLButtonDblClk = 0x0203; + private const int TpmLeftAlign = 0x0000; + private const int TpmBottomAlign = 0x0020; + private const int TpmRightButton = 0x0002; + private const int TpmReturnCmd = 0x0100; + private const int MfString = 0x0000; + private const int MfSeparator = 0x0800; + private const int MfDisabled = 0x0002; + private const int MfGray = 0x0001; + private const int NifMessage = 0x00000001; + private const int NifIcon = 0x00000002; + private const int NifTip = 0x00000004; + private const int NifGuid = 0x00000020; + private const int NimAdd = 0x00000000; + private const int NimModify = 0x00000001; + private const int NimDelete = 0x00000002; + private const int TrayCallbackMessage = WmApp + 1; + private const string WindowClassName = "WinoMail.NativeTrayIconWindow"; + private static readonly Guid TrayIconGuid = new("6E1330D0-22D5-4F0B-A3BF-C9B2AE536F77"); + private static readonly object ClassLock = new(); + private static readonly Dictionary Instances = []; + private static bool _windowClassRegistered; + private static ushort _windowClassAtom; + private static uint _taskbarCreatedMessage; + private static readonly WindowProcDelegate WindowProc = StaticWindowProc; + + private readonly DispatcherQueue _dispatcherQueue; + private readonly string _iconPath; + private readonly Func> _menuFactory; + private readonly Func _primaryAction; + private readonly string _toolTipText; + + private nint _messageWindowHandle; + private nint _iconHandle; + private bool _isCreated; + private bool _isDisposed; + + public NativeTrayIcon( + DispatcherQueue dispatcherQueue, + string iconPath, + string toolTipText, + Func> menuFactory, + Func primaryAction) + { + _dispatcherQueue = dispatcherQueue; + _iconPath = iconPath; + _toolTipText = toolTipText; + _menuFactory = menuFactory; + _primaryAction = primaryAction; + } + + public void Create() + { + if (_isDisposed || _isCreated) + return; + + EnsureWindowClassRegistered(); + + _messageWindowHandle = CreateWindowExW( + 0, + WindowClassName, + string.Empty, + 0, + 0, + 0, + 0, + 0, + nint.Zero, + nint.Zero, + GetModuleHandleW(null), + nint.Zero); + + if (_messageWindowHandle == nint.Zero) + throw new InvalidOperationException("Failed to create native tray icon message window."); + + lock (Instances) + { + Instances[_messageWindowHandle] = this; + } + + _iconHandle = LoadImageW(nint.Zero, _iconPath, ImageIcon, 0, 0, LoadFromFile); + if (_iconHandle == nint.Zero) + throw new InvalidOperationException($"Failed to load tray icon from '{_iconPath}'."); + + AddOrUpdateIcon(NimAdd); + _isCreated = true; + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + + if (_messageWindowHandle != nint.Zero) + { + RemoveIcon(); + DestroyWindow(_messageWindowHandle); + lock (Instances) + { + Instances.Remove(_messageWindowHandle); + } + + _messageWindowHandle = nint.Zero; + } + + if (_iconHandle != nint.Zero) + { + DestroyIcon(_iconHandle); + _iconHandle = nint.Zero; + } + + _isCreated = false; + } + + private void AddOrUpdateIcon(int message) + { + var notifyIconData = CreateNotifyIconData(); + Shell_NotifyIconW(message, ref notifyIconData); + } + + private void RemoveIcon() + { + var notifyIconData = CreateNotifyIconData(); + Shell_NotifyIconW(NimDelete, ref notifyIconData); + } + + private NOTIFYICONDATAW CreateNotifyIconData() + { + return new NOTIFYICONDATAW + { + cbSize = (uint)Marshal.SizeOf(), + hWnd = _messageWindowHandle, + uID = 1, + uFlags = NifMessage | NifIcon | NifTip | NifGuid, + uCallbackMessage = TrayCallbackMessage, + hIcon = _iconHandle, + szTip = _toolTipText, + guidItem = TrayIconGuid + }; + } + + private void ShowContextMenu() + { + var menuHandle = CreatePopupMenu(); + if (menuHandle == nint.Zero) + return; + + try + { + var menuItems = _menuFactory(); + var commandMap = new Dictionary>(); + uint commandId = 1; + + foreach (var menuItem in menuItems) + { + if (menuItem.IsSeparator) + { + AppendMenuW(menuHandle, MfSeparator, 0, null); + continue; + } + + uint flags = MfString; + if (!menuItem.IsEnabled) + flags |= MfDisabled | MfGray; + + AppendMenuW(menuHandle, flags, commandId, menuItem.Text); + + if (menuItem.IsDefault) + SetMenuDefaultItem(menuHandle, commandId, false); + + if (menuItem.IsEnabled) + commandMap[commandId] = menuItem.Action; + + commandId++; + } + + SetForegroundWindow(_messageWindowHandle); + + if (!GetCursorPos(out var point)) + return; + + var selectedCommandId = TrackPopupMenuEx( + menuHandle, + TpmLeftAlign | TpmBottomAlign | TpmRightButton | TpmReturnCmd, + point.X, + point.Y, + _messageWindowHandle, + nint.Zero); + + PostMessageW(_messageWindowHandle, WmNull, 0, 0); + + if (selectedCommandId != 0 && commandMap.TryGetValue((uint)selectedCommandId, out var action)) + InvokeAction(action); + } + finally + { + DestroyMenu(menuHandle); + } + } + + private void InvokePrimaryAction() => InvokeAction(_primaryAction); + + private void InvokeAction(Func action) + { + _dispatcherQueue.TryEnqueue(async () => + { + try + { + await action(); + } + catch (Exception ex) + { + Log.Error(ex, "Native tray icon action failed."); + } + }); + } + + private nint HandleWindowMessage(uint message, nuint wParam, nint lParam) + { + if (message == _taskbarCreatedMessage) + { + if (_isCreated && !_isDisposed) + AddOrUpdateIcon(NimAdd); + + return nint.Zero; + } + + if (message == TrayCallbackMessage) + { + switch ((int)lParam) + { + case WmRButtonUp: + ShowContextMenu(); + return nint.Zero; + case WmLButtonDblClk: + InvokePrimaryAction(); + return nint.Zero; + } + } + + if (message == WmCommand || message == WmClose || message == WmDestroy) + return nint.Zero; + + return DefWindowProcW(_messageWindowHandle, message, wParam, lParam); + } + + private static void EnsureWindowClassRegistered() + { + lock (ClassLock) + { + if (_windowClassRegistered) + return; + + _taskbarCreatedMessage = RegisterWindowMessageW("TaskbarCreated"); + + var windowClass = new WNDCLASSW + { + lpfnWndProc = WindowProc, + hInstance = GetModuleHandleW(null), + lpszClassName = WindowClassName + }; + + _windowClassAtom = RegisterClassW(ref windowClass); + if (_windowClassAtom == 0) + throw new InvalidOperationException("Failed to register native tray icon window class."); + + _windowClassRegistered = true; + } + } + + private static nint StaticWindowProc(nint windowHandle, uint message, nuint wParam, nint lParam) + { + lock (Instances) + { + if (Instances.TryGetValue(windowHandle, out var instance)) + return instance.HandleWindowMessage(message, wParam, lParam); + } + + return DefWindowProcW(windowHandle, message, wParam, lParam); + } + + internal sealed record NativeTrayMenuItem( + string Text, + Func Action, + bool IsSeparator = false, + bool IsDefault = false, + bool IsEnabled = true) + { + public static NativeTrayMenuItem Separator() => new(string.Empty, () => Task.CompletedTask, IsSeparator: true); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WNDCLASSW + { + public uint style; + public WindowProcDelegate lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public nint hInstance; + public nint hIcon; + public nint hCursor; + public nint hbrBackground; + [MarshalAs(UnmanagedType.LPWStr)] + public string? lpszMenuName; + [MarshalAs(UnmanagedType.LPWStr)] + public string lpszClassName; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct NOTIFYICONDATAW + { + public uint cbSize; + public nint hWnd; + public uint uID; + public uint uFlags; + public uint uCallbackMessage; + public nint hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public uint dwState; + public uint dwStateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public uint uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public uint dwInfoFlags; + public Guid guidItem; + public nint hBalloonIcon; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int X; + public int Y; + } + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate nint WindowProcDelegate(nint windowHandle, uint message, nuint wParam, nint lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern ushort RegisterClassW(ref WNDCLASSW windowClass); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern nint CreateWindowExW( + int exStyle, + string className, + string windowName, + int style, + int x, + int y, + int width, + int height, + nint parentHandle, + nint menuHandle, + nint instanceHandle, + nint parameter); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool DestroyWindow(nint windowHandle); + + [DllImport("user32.dll")] + private static extern nint DefWindowProcW(nint windowHandle, uint message, nuint wParam, nint lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern nint GetModuleHandleW(string? moduleName); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + private static extern bool Shell_NotifyIconW(int message, ref NOTIFYICONDATAW notifyIconData); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern nint LoadImageW( + nint instanceHandle, + string name, + int imageType, + int desiredWidth, + int desiredHeight, + int loadFlags); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool DestroyIcon(nint iconHandle); + + [DllImport("user32.dll", SetLastError = true)] + private static extern nint CreatePopupMenu(); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool AppendMenuW(nint menuHandle, uint flags, uint itemId, string? newItem); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool DestroyMenu(nint menuHandle); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetMenuDefaultItem(nint menuHandle, uint itemId, bool byPosition); + + [DllImport("user32.dll", SetLastError = true)] + private static extern int TrackPopupMenuEx( + nint menuHandle, + int flags, + int x, + int y, + nint windowHandle, + nint reserved); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool GetCursorPos(out POINT point); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetForegroundWindow(nint windowHandle); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool PostMessageW(nint windowHandle, uint message, nuint wParam, nint lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern uint RegisterWindowMessageW(string message); +} diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml index a71cab7a..da45b6b4 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml +++ b/Wino.Mail.WinUI/ShellWindow.xaml @@ -9,7 +9,6 @@ xmlns:helpers="using:Wino.Helpers" xmlns:local="using:Wino.Mail.WinUI" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:notifyicon="using:H.NotifyIcon" xmlns:syncModels="using:Wino.Core.Domain.Models.Synchronization" xmlns:winuiex="using:WinUIEx" Title="ShellWindow" @@ -185,22 +184,5 @@ VerticalAlignment="Top" IsClosable="False" IsOpen="False" /> - - - - - - - - - - - - diff --git a/Wino.Mail.WinUI/ShellWindow.xaml.cs b/Wino.Mail.WinUI/ShellWindow.xaml.cs index cb14933b..08ad508b 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml.cs +++ b/Wino.Mail.WinUI/ShellWindow.xaml.cs @@ -2,7 +2,6 @@ using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; -using System.Windows.Input; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; @@ -40,12 +39,6 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, private IMailDialogService MailDialogService { get; } = WinoApplication.Current.Services.GetRequiredService(); private IWinoAccountProfileService WinoAccountProfileService { get; } = WinoApplication.Current.Services.GetRequiredService(); - public ICommand ShowWinoCommand { get; set; } - public ICommand ShowWinoCalendarCommand { get; set; } - public ICommand ShowWinoContactsCommand { get; set; } - public ICommand RestoreCurrentModeCommand { get; set; } - public ICommand ExitWinoCommand { get; set; } - public ObservableCollection SyncActionItems { get; } = new(); private bool _calendarReminderServerStartAttempted; @@ -68,16 +61,8 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, // Register global mouse button listener for back button RegisterMouseBackButtonListener(); - ShowWinoCommand = new RelayCommand(() => RestoreAndSwitchMode(WinoApplicationMode.Mail)); - ShowWinoCalendarCommand = new RelayCommand(() => RestoreAndSwitchMode(WinoApplicationMode.Calendar)); - ShowWinoContactsCommand = new RelayCommand(() => RestoreAndSwitchMode(WinoApplicationMode.Contacts)); - RestoreCurrentModeCommand = new RelayCommand(() => RestoreAndSwitchMode(StatePersistanceService.ApplicationMode)); - ExitWinoCommand = new RelayCommand(ForceClose); - this.SetIcon("Assets/Wino_Icon.ico"); Title = StatePersistanceService.AppModeTitle; - - SystemTrayIcon.ForceCreate(); } private void ConfigureTitleBar() @@ -274,49 +259,18 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, private void OnAppWindowClosing(object sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs e) { + if ((Application.Current as App)?.IsExiting == true) + return; + e.Cancel = true; - MinimizeToTray(); + var windowManager = WinoApplication.Current.Services.GetService(); + windowManager?.HideWindow(this); } private void OnWindowClosed(object sender, WindowEventArgs e) { - SystemTrayIcon?.Dispose(); - } - - private void MinimizeToTray() - { - this.Hide(); - SystemTrayIcon.ForceCreate(); - } - - private void RestoreFromTray() - { - - this.Show(); - BringToFront(); - } - - private void RestoreAndSwitchMode(WinoApplicationMode mode) - { - NavigationService.ChangeApplicationMode(mode); - RestoreFromTray(); - } - - public void ForceClose() - { - // Unsubscribe from the closing event to avoid infinite loop AppWindow.Closing -= OnAppWindowClosing; - - // Clean up system tray - SystemTrayIcon?.Dispose(); - UnregisterRecipients(); - - var windowManager = WinoApplication.Current.Services.GetService(); - windowManager?.CloseAllWindows(); - - // Exit the application - Application.Current.Exit(); } private void RegisterRecipients() diff --git a/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml b/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml index 7ba1fe99..dbf939e1 100644 --- a/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml +++ b/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml @@ -19,7 +19,7 @@ @@ -78,15 +78,14 @@ - - - + - + diff --git a/Wino.Mail.WinUI/WelcomeWindow.xaml.cs b/Wino.Mail.WinUI/WelcomeWindow.xaml.cs index 63c16556..ce7065c4 100644 --- a/Wino.Mail.WinUI/WelcomeWindow.xaml.cs +++ b/Wino.Mail.WinUI/WelcomeWindow.xaml.cs @@ -1,13 +1,17 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Interfaces; +using Wino.Mail.WinUI.Interfaces; using WinUIEx; namespace Wino.Mail.WinUI; public sealed partial class WelcomeWindow : WindowEx { + private bool _allowClose; + public Frame GetRootFrame() => RootFrame; public WelcomeWindow() @@ -20,6 +24,7 @@ public sealed partial class WelcomeWindow : WindowEx this.SetIcon("Assets/Wino_Icon.ico"); ConfigureWindowChrome(); + AppWindow.Closing += OnAppWindowClosing; } private void ConfigureWindowChrome() @@ -34,4 +39,20 @@ public sealed partial class WelcomeWindow : WindowEx var themeService = WinoApplication.Current.Services.GetService(); themeService?.UpdateSystemCaptionButtonColors(); } + + private void OnAppWindowClosing(object sender, AppWindowClosingEventArgs e) + { + if (_allowClose || (Application.Current as App)?.IsExiting == true) + return; + + e.Cancel = true; + + var windowManager = WinoApplication.Current.Services.GetService(); + windowManager?.HideWindow(this); + } + + public void AllowClose() + { + _allowClose = true; + } } diff --git a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj index 1b10618c..2fe9c79e 100644 --- a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj +++ b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj @@ -223,7 +223,6 @@ -