Native tray icon implementation.

This commit is contained in:
Burak Kaan Köse
2026-03-20 12:43:09 +01:00
parent 4a20ea2577
commit 1fe569e0ac
8 changed files with 642 additions and 92 deletions
@@ -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.",
+172 -15
View File
@@ -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<Guid, int> _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<NativeTrayIcon.NativeTrayMenuItem> BuildTrayMenu()
{
List<NativeTrayIcon.NativeTrayMenuItem> 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<IWinoWindowManager>();
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<IWinoWindowManager>();
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<IWinoWindowManager>();
if (windowManager.GetWindow(WinoWindowKind.Welcome) is not WelcomeWindow welcomeWindow)
return;
welcomeWindow.AllowClose();
welcomeWindow.Close();
}
private async Task ActivateWindowAsync(WindowEx window)
{
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
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<IWinoWindowManager>().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<IAccountService>();
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<IWinoWindowManager>();
var navigationService = Services.GetRequiredService<INavigationService>();
@@ -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<WelcomeWizardContext>().Reset();
StopAutoSynchronizationLoop();
CreateWelcomeWindow();
windowManager.HideWindow(WinoWindowKind.Shell);
await NewThemeService.ApplyThemeToActiveWindowAsync();
MainWindow?.Activate();
if (MainWindow != null)
await ActivateWindowAsync(MainWindow);
});
}
+437
View File
@@ -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<nint, NativeTrayIcon> 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<IReadOnlyList<NativeTrayMenuItem>> _menuFactory;
private readonly Func<Task> _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<IReadOnlyList<NativeTrayMenuItem>> menuFactory,
Func<Task> 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<NOTIFYICONDATAW>(),
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, Func<Task>>();
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<Task> 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<Task> 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);
}
-18
View File
@@ -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" />
<notifyicon:TaskbarIcon
x:Name="SystemTrayIcon"
ContextMenuMode="PopupMenu"
DoubleClickCommand="{x:Bind RestoreCurrentModeCommand}"
IconSource="/Assets/Wino_Icon.ico"
NoLeftClickDelay="True">
<notifyicon:TaskbarIcon.ContextFlyout>
<MenuFlyout AreOpenCloseAnimationsEnabled="False">
<MenuFlyoutItem Command="{x:Bind ShowWinoCommand}" Text="{x:Bind domain:Translator.SystemTrayMenu_ShowWino}" />
<MenuFlyoutItem Command="{x:Bind ShowWinoCalendarCommand}" Text="{x:Bind domain:Translator.SystemTrayMenu_ShowWinoCalendar}" />
<!--<MenuFlyoutItem Command="{x:Bind ShowWinoContactsCommand}" Text="{x:Bind domain:Translator.SystemTrayMenu_ShowWinoContacts}" />-->
<MenuFlyoutSeparator />
<MenuFlyoutItem Command="{x:Bind ExitWinoCommand}" Text="{x:Bind domain:Translator.SystemTrayMenu_ExitWino}" />
</MenuFlyout>
</notifyicon:TaskbarIcon.ContextFlyout>
</notifyicon:TaskbarIcon>
</Grid>
</winuiex:WindowEx>
+5 -51
View File
@@ -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<IMailDialogService>();
private IWinoAccountProfileService WinoAccountProfileService { get; } = WinoApplication.Current.Services.GetRequiredService<IWinoAccountProfileService>();
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<SynchronizationActionItem> 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<IWinoWindowManager>();
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<IWinoWindowManager>();
windowManager?.CloseAllWindows();
// Exit the application
Application.Current.Exit();
}
private void RegisterRecipients()
@@ -19,7 +19,7 @@
<StackPanel
MaxWidth="480"
Margin="0,24,0,24"
HorizontalAlignment="Center"
HorizontalAlignment="Stretch"
Spacing="20">
<!-- Title -->
@@ -78,15 +78,14 @@
<!-- Provider List -->
<ItemsView
HorizontalContentAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}"
SelectionChanged="ProviderSelectionChanged"
SelectionMode="Single">
<ItemsView.Layout>
<UniformGridLayout Orientation="Vertical" />
</ItemsView.Layout>
<ItemsView.ItemTemplate>
<DataTemplate x:DataType="interfaces:IProviderDetail">
<ItemContainer Padding="12,10">
<ItemContainer Padding="12,10" HorizontalAlignment="Stretch">
<Grid Padding="16" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
+21
View File
@@ -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<INewThemeService>();
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<IWinoWindowManager>();
windowManager?.HideWindow(this);
}
public void AllowClose()
{
_allowClose = true;
}
}
-1
View File
@@ -223,7 +223,6 @@
<PackageReference Include="EmailValidation" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="H.NotifyIcon.WinUI" />
<PackageReference Include="Wino.Mail.Contracts" />
</ItemGroup>
<ItemGroup>