Native tray icon implementation.
This commit is contained in:
@@ -1041,8 +1041,9 @@
|
|||||||
"SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.",
|
"SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.",
|
||||||
"SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.",
|
"SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.",
|
||||||
"SystemFolderConfigSetupSuccess_Title": "System Folders Setup",
|
"SystemFolderConfigSetupSuccess_Title": "System Folders Setup",
|
||||||
"SystemTrayMenu_ShowWino": "Open Wino Mail",
|
"SystemTrayMenu_Open": "Open",
|
||||||
"SystemTrayMenu_ShowWinoCalendar": "Open Wino Calendar",
|
"SystemTrayMenu_ShowWino": "Open Mail",
|
||||||
|
"SystemTrayMenu_ShowWinoCalendar": "Open Calendar",
|
||||||
"SystemTrayMenu_ExitWino": "Exit",
|
"SystemTrayMenu_ExitWino": "Exit",
|
||||||
"TestingImapConnectionMessage": "Testing server connection...",
|
"TestingImapConnectionMessage": "Testing server connection...",
|
||||||
"TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.",
|
"TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.",
|
||||||
|
|||||||
+172
-15
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Toolkit.Uwp.Notifications;
|
using Microsoft.Toolkit.Uwp.Notifications;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.Windows.AppLifecycle;
|
using Microsoft.Windows.AppLifecycle;
|
||||||
using Microsoft.Windows.AppNotifications;
|
using Microsoft.Windows.AppNotifications;
|
||||||
@@ -57,9 +58,14 @@ public partial class App : WinoApplication,
|
|||||||
private IPreferencesService? _preferencesService;
|
private IPreferencesService? _preferencesService;
|
||||||
private IAccountService? _accountService;
|
private IAccountService? _accountService;
|
||||||
private bool _windowManagerConfigured;
|
private bool _windowManagerConfigured;
|
||||||
|
private bool _hasConfiguredAccounts;
|
||||||
|
private bool _isExiting;
|
||||||
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
||||||
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
||||||
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
||||||
|
private NativeTrayIcon? _trayIcon;
|
||||||
|
|
||||||
|
internal bool IsExiting => _isExiting;
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
@@ -117,6 +123,142 @@ public partial class App : WinoApplication,
|
|||||||
InitializeNavigationDispatcher();
|
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)
|
public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
@@ -228,8 +370,10 @@ public partial class App : WinoApplication,
|
|||||||
_accountService = Services.GetRequiredService<IAccountService>();
|
_accountService = Services.GetRequiredService<IAccountService>();
|
||||||
|
|
||||||
EnsureWindowManagerConfigured();
|
EnsureWindowManagerConfigured();
|
||||||
|
EnsureTrayIconCreated();
|
||||||
|
|
||||||
var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any();
|
var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any();
|
||||||
|
_hasConfiguredAccounts = hasAnyAccount;
|
||||||
if (!IsStartupTaskLaunch() && !hasAnyAccount)
|
if (!IsStartupTaskLaunch() && !hasAnyAccount)
|
||||||
{
|
{
|
||||||
CreateWelcomeWindow();
|
CreateWelcomeWindow();
|
||||||
@@ -254,16 +398,25 @@ public partial class App : WinoApplication,
|
|||||||
// Check if launched by startup task.
|
// Check if launched by startup task.
|
||||||
bool isStartupTaskLaunch = IsStartupTaskLaunch();
|
bool isStartupTaskLaunch = IsStartupTaskLaunch();
|
||||||
|
|
||||||
// Create the window (needed for system tray icon even in startup task scenario).
|
if (isStartupTaskLaunch && !hasAnyAccount)
|
||||||
CreateWindow(args);
|
{
|
||||||
|
CreateWelcomeWindow();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CreateWindow(args);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize theme service after window creation.
|
// Initialize theme service after window creation.
|
||||||
// Theme service requires the window to exist to properly load and apply themes.
|
// Theme service requires the window to exist to properly load and apply themes.
|
||||||
await NewThemeService.InitializeAsync();
|
await NewThemeService.InitializeAsync();
|
||||||
|
|
||||||
// Wino account loading and activation.
|
if (hasAnyAccount)
|
||||||
await LoadInitialWinoAccountAsync();
|
{
|
||||||
await HandlePostActivationAsync(AppInstance.GetCurrent().GetActivatedEventArgs());
|
// Wino account loading and activation.
|
||||||
|
await LoadInitialWinoAccountAsync();
|
||||||
|
await HandlePostActivationAsync(AppInstance.GetCurrent().GetActivatedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
LogActivation("Theme service initialized.");
|
LogActivation("Theme service initialized.");
|
||||||
|
|
||||||
@@ -477,7 +630,7 @@ public partial class App : WinoApplication,
|
|||||||
if (mailItem == null)
|
if (mailItem == null)
|
||||||
{
|
{
|
||||||
LogActivation("Mail item not found. Exiting.");
|
LogActivation("Mail item not found. Exiting.");
|
||||||
Application.Current.Exit();
|
ExitApplication();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +657,7 @@ public partial class App : WinoApplication,
|
|||||||
if (_synchronizationManager == null)
|
if (_synchronizationManager == null)
|
||||||
{
|
{
|
||||||
LogActivation("Synchronization manager is not initialized. Exiting.");
|
LogActivation("Synchronization manager is not initialized. Exiting.");
|
||||||
Application.Current.Exit();
|
ExitApplication();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +703,7 @@ public partial class App : WinoApplication,
|
|||||||
LogActivation("Toast action handling complete. Exiting app.");
|
LogActivation("Toast action handling complete. Exiting app.");
|
||||||
|
|
||||||
// Exit the app after synchronization is complete.
|
// 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)
|
public void Receive(AccountCreatedMessage message)
|
||||||
{
|
{
|
||||||
|
_hasConfiguredAccounts = true;
|
||||||
|
|
||||||
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
var navigationService = Services.GetRequiredService<INavigationService>();
|
var navigationService = Services.GetRequiredService<INavigationService>();
|
||||||
|
|
||||||
@@ -742,10 +897,11 @@ public partial class App : WinoApplication,
|
|||||||
{
|
{
|
||||||
// Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher.
|
// Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher.
|
||||||
CreateWindow(null);
|
CreateWindow(null);
|
||||||
windowManager.HideWindow(WinoWindowKind.Welcome);
|
CloseWelcomeWindowIfPresent();
|
||||||
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
|
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
|
||||||
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
if (MainWindow != null)
|
||||||
MainWindow?.Activate();
|
await ActivateWindowAsync(MainWindow);
|
||||||
|
|
||||||
RestartAutoSynchronizationLoop();
|
RestartAutoSynchronizationLoop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -761,15 +917,16 @@ public partial class App : WinoApplication,
|
|||||||
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
||||||
{
|
{
|
||||||
var accounts = await _accountService!.GetAccountsAsync();
|
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
|
// All accounts removed — go back to welcome wizard from step 1
|
||||||
Services.GetRequiredService<WelcomeWizardContext>().Reset();
|
Services.GetRequiredService<WelcomeWizardContext>().Reset();
|
||||||
StopAutoSynchronizationLoop();
|
StopAutoSynchronizationLoop();
|
||||||
CreateWelcomeWindow();
|
CreateWelcomeWindow();
|
||||||
windowManager.HideWindow(WinoWindowKind.Shell);
|
windowManager.HideWindow(WinoWindowKind.Shell);
|
||||||
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
if (MainWindow != null)
|
||||||
MainWindow?.Activate();
|
await ActivateWindowAsync(MainWindow);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:local="using:Wino.Mail.WinUI"
|
xmlns:local="using:Wino.Mail.WinUI"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:notifyicon="using:H.NotifyIcon"
|
|
||||||
xmlns:syncModels="using:Wino.Core.Domain.Models.Synchronization"
|
xmlns:syncModels="using:Wino.Core.Domain.Models.Synchronization"
|
||||||
xmlns:winuiex="using:WinUIEx"
|
xmlns:winuiex="using:WinUIEx"
|
||||||
Title="ShellWindow"
|
Title="ShellWindow"
|
||||||
@@ -185,22 +184,5 @@
|
|||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
IsClosable="False"
|
IsClosable="False"
|
||||||
IsOpen="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>
|
</Grid>
|
||||||
</winuiex:WindowEx>
|
</winuiex:WindowEx>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
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 IMailDialogService MailDialogService { get; } = WinoApplication.Current.Services.GetRequiredService<IMailDialogService>();
|
||||||
private IWinoAccountProfileService WinoAccountProfileService { get; } = WinoApplication.Current.Services.GetRequiredService<IWinoAccountProfileService>();
|
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();
|
public ObservableCollection<SynchronizationActionItem> SyncActionItems { get; } = new();
|
||||||
private bool _calendarReminderServerStartAttempted;
|
private bool _calendarReminderServerStartAttempted;
|
||||||
|
|
||||||
@@ -68,16 +61,8 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
|||||||
// Register global mouse button listener for back button
|
// Register global mouse button listener for back button
|
||||||
RegisterMouseBackButtonListener();
|
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");
|
this.SetIcon("Assets/Wino_Icon.ico");
|
||||||
Title = StatePersistanceService.AppModeTitle;
|
Title = StatePersistanceService.AppModeTitle;
|
||||||
|
|
||||||
SystemTrayIcon.ForceCreate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureTitleBar()
|
private void ConfigureTitleBar()
|
||||||
@@ -274,49 +259,18 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
|||||||
|
|
||||||
private void OnAppWindowClosing(object sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs e)
|
private void OnAppWindowClosing(object sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs e)
|
||||||
{
|
{
|
||||||
|
if ((Application.Current as App)?.IsExiting == true)
|
||||||
|
return;
|
||||||
|
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
MinimizeToTray();
|
var windowManager = WinoApplication.Current.Services.GetService<IWinoWindowManager>();
|
||||||
|
windowManager?.HideWindow(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWindowClosed(object sender, WindowEventArgs e)
|
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;
|
AppWindow.Closing -= OnAppWindowClosing;
|
||||||
|
|
||||||
// Clean up system tray
|
|
||||||
SystemTrayIcon?.Dispose();
|
|
||||||
|
|
||||||
UnregisterRecipients();
|
UnregisterRecipients();
|
||||||
|
|
||||||
var windowManager = WinoApplication.Current.Services.GetService<IWinoWindowManager>();
|
|
||||||
windowManager?.CloseAllWindows();
|
|
||||||
|
|
||||||
// Exit the application
|
|
||||||
Application.Current.Exit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterRecipients()
|
private void RegisterRecipients()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<StackPanel
|
<StackPanel
|
||||||
MaxWidth="480"
|
MaxWidth="480"
|
||||||
Margin="0,24,0,24"
|
Margin="0,24,0,24"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Stretch"
|
||||||
Spacing="20">
|
Spacing="20">
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
@@ -78,15 +78,14 @@
|
|||||||
|
|
||||||
<!-- Provider List -->
|
<!-- Provider List -->
|
||||||
<ItemsView
|
<ItemsView
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}"
|
ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}"
|
||||||
SelectionChanged="ProviderSelectionChanged"
|
SelectionChanged="ProviderSelectionChanged"
|
||||||
SelectionMode="Single">
|
SelectionMode="Single">
|
||||||
<ItemsView.Layout>
|
|
||||||
<UniformGridLayout Orientation="Vertical" />
|
|
||||||
</ItemsView.Layout>
|
|
||||||
<ItemsView.ItemTemplate>
|
<ItemsView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="interfaces:IProviderDetail">
|
<DataTemplate x:DataType="interfaces:IProviderDetail">
|
||||||
<ItemContainer Padding="12,10">
|
<ItemContainer Padding="12,10" HorizontalAlignment="Stretch">
|
||||||
<Grid Padding="16" ColumnSpacing="12">
|
<Grid Padding="16" ColumnSpacing="12">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Windowing;
|
using Microsoft.UI.Windowing;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Mail.WinUI.Interfaces;
|
||||||
using WinUIEx;
|
using WinUIEx;
|
||||||
|
|
||||||
namespace Wino.Mail.WinUI;
|
namespace Wino.Mail.WinUI;
|
||||||
|
|
||||||
public sealed partial class WelcomeWindow : WindowEx
|
public sealed partial class WelcomeWindow : WindowEx
|
||||||
{
|
{
|
||||||
|
private bool _allowClose;
|
||||||
|
|
||||||
public Frame GetRootFrame() => RootFrame;
|
public Frame GetRootFrame() => RootFrame;
|
||||||
|
|
||||||
public WelcomeWindow()
|
public WelcomeWindow()
|
||||||
@@ -20,6 +24,7 @@ public sealed partial class WelcomeWindow : WindowEx
|
|||||||
this.SetIcon("Assets/Wino_Icon.ico");
|
this.SetIcon("Assets/Wino_Icon.ico");
|
||||||
|
|
||||||
ConfigureWindowChrome();
|
ConfigureWindowChrome();
|
||||||
|
AppWindow.Closing += OnAppWindowClosing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureWindowChrome()
|
private void ConfigureWindowChrome()
|
||||||
@@ -34,4 +39,20 @@ public sealed partial class WelcomeWindow : WindowEx
|
|||||||
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
|
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
|
||||||
themeService?.UpdateSystemCaptionButtonColors();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,7 +223,6 @@
|
|||||||
<PackageReference Include="EmailValidation" />
|
<PackageReference Include="EmailValidation" />
|
||||||
<PackageReference Include="System.Drawing.Common" />
|
<PackageReference Include="System.Drawing.Common" />
|
||||||
<PackageReference Include="WinUIEx" />
|
<PackageReference Include="WinUIEx" />
|
||||||
<PackageReference Include="H.NotifyIcon.WinUI" />
|
|
||||||
<PackageReference Include="Wino.Mail.Contracts" />
|
<PackageReference Include="Wino.Mail.Contracts" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user