diff --git a/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs b/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs index 2a234df8..ca84b5e1 100644 --- a/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs +++ b/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs @@ -7,25 +7,32 @@ internal static class AppModeActivationResolver { public static WinoApplicationMode Resolve(string? launchArguments, string? tileId, string? appId, WinoApplicationMode defaultMode = WinoApplicationMode.Mail) { - if (TryResolveFromText(launchArguments, out var mode)) + if (TryResolveFromText(launchArguments, defaultMode, out var mode)) return mode; - if (TryResolveFromText(tileId, out mode)) + if (TryResolveFromText(tileId, defaultMode, out mode)) return mode; - if (TryResolveFromText(appId, out mode)) + if (TryResolveFromText(appId, defaultMode, out mode)) return mode; return defaultMode; } - private static bool TryResolveFromText(string? value, out WinoApplicationMode mode) + private static bool TryResolveFromText(string? value, WinoApplicationMode defaultMode, out WinoApplicationMode mode) { - mode = WinoApplicationMode.Mail; + mode = defaultMode; if (string.IsNullOrWhiteSpace(value)) return false; + if (Contains(value, "--mode=toggle-default") || + Contains(value, "mode=toggle-default")) + { + mode = GetOpposite(defaultMode); + return true; + } + if (Contains(value, "wino-calendar") || Contains(value, "--mode=calendar") || Contains(value, "mode=calendar") || @@ -54,4 +61,9 @@ internal static class AppModeActivationResolver private static bool EqualsToken(string source, string token) => string.Equals(source.Trim(), token, StringComparison.OrdinalIgnoreCase); + + private static WinoApplicationMode GetOpposite(WinoApplicationMode defaultMode) + => defaultMode == WinoApplicationMode.Mail + ? WinoApplicationMode.Calendar + : WinoApplicationMode.Mail; } diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 7c0a3b3e..031915f5 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -38,6 +38,7 @@ public partial class App : WinoApplication, IRecipient { private const int InboxSyncsPerFullSync = 20; + private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default"; private ISynchronizationManager? _synchronizationManager; private IPreferencesService? _preferencesService; private IAccountService? _accountService; @@ -136,7 +137,7 @@ public partial class App : WinoApplication, { base.OnLaunched(args); - // Always register notification callbacks for both app entries (Mail and Calendar). + // Always register notification callbacks. TryRegisterAppNotifications(); // Initialize required services regardless of launch activation type. @@ -429,7 +430,14 @@ public partial class App : WinoApplication, if (activationArgs.Kind == ExtendedActivationKind.Launch && activationArgs.Data is ILaunchActivatedEventArgs launchArgs) { - shellWindow.HandleAppActivation(launchArgs.Arguments, launchArgs.TileId, Environment.CommandLine); + var launchArguments = launchArgs.Arguments; + + if (Program.TryConsumeCurrentProcessAlternateModeOverride()) + { + launchArguments = AppendLaunchArgument(launchArguments, ToggleDefaultModeLaunchArgument); + } + + shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId, Environment.CommandLine); return; } @@ -662,7 +670,14 @@ public partial class App : WinoApplication, if (args.Kind == ExtendedActivationKind.Launch && args.Data is ILaunchActivatedEventArgs launchArgs) { - shellWindow.HandleAppActivation(launchArgs.Arguments, launchArgs.TileId); + var launchArguments = launchArgs.Arguments; + + if (Program.TryConsumeRedirectedAlternateModeOverride()) + { + launchArguments = AppendLaunchArgument(launchArguments, ToggleDefaultModeLaunchArgument); + } + + shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId); } else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode)) { @@ -680,6 +695,13 @@ public partial class App : WinoApplication, private static string GetModeLaunchArgument(WinoApplicationMode mode) => mode == WinoApplicationMode.Calendar ? "--mode=calendar" : "--mode=mail"; + private static string AppendLaunchArgument(string? launchArguments, string launchArgument) + { + return string.IsNullOrWhiteSpace(launchArguments) + ? launchArgument + : $"{launchArguments} {launchArgument}"; + } + private static bool TryResolveActivationMode(AppActivationArguments activationArgs, WinoApplicationMode defaultMode, out WinoApplicationMode mode) { mode = defaultMode; diff --git a/Wino.Mail.WinUI/Package.appxmanifest b/Wino.Mail.WinUI/Package.appxmanifest index b1e416a8..d55d2c6c 100644 --- a/Wino.Mail.WinUI/Package.appxmanifest +++ b/Wino.Mail.WinUI/Package.appxmanifest @@ -90,6 +90,19 @@ + + + + Calendar Protocol + + + + + + Calendar Protocol (Secure) + + + @@ -99,48 +112,18 @@ + + + + + Assets\AppEntries\CalendarAssets\Square44x44Logo.png + + .ics + + + - - - - - - - - - - - - Calendar Protocol - - - - - - Calendar Protocol (Secure) - - - - - - - Assets\AppEntries\CalendarAssets\Square44x44Logo.png - - .ics - - - - - diff --git a/Wino.Mail.WinUI/Program.cs b/Wino.Mail.WinUI/Program.cs index 091e5dec..c6247346 100644 --- a/Wino.Mail.WinUI/Program.cs +++ b/Wino.Mail.WinUI/Program.cs @@ -11,6 +11,13 @@ namespace Wino.Mail.WinUI; public class Program { + private const string SingleInstanceKey = "WinoMailSingleInstance"; + private const string ForceAlternateModeSignalEventName = "Local\\WinoMailForceAlternateMode"; + private const int VkControl = 0x11; + + private static bool _forceAlternateModeOnLaunch; + private static EventWaitHandle? _forceAlternateModeSignalHandle; + [STAThread] static int Main(string[] args) { @@ -35,15 +42,24 @@ public class Program { bool isRedirect = false; AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs(); - AppInstance keyInstance = AppInstance.FindOrRegisterForKey("WinoMailSingleInstance"); + _forceAlternateModeOnLaunch = args.Kind == ExtendedActivationKind.Launch && IsCtrlKeyDown(); + + AppInstance keyInstance = AppInstance.FindOrRegisterForKey(SingleInstanceKey); if (keyInstance.IsCurrent) { + EnsureAlternateModeOverrideSignalHandle(); keyInstance.Activated += OnActivated; } else { isRedirect = true; + + if (_forceAlternateModeOnLaunch) + { + SignalForceAlternateMode(); + } + RedirectActivationTo(args, keyInstance); } @@ -66,8 +82,83 @@ public class Program [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] + private static extern short GetAsyncKeyState(int vKey); + private static IntPtr redirectEventHandle = IntPtr.Zero; + public static bool TryConsumeCurrentProcessAlternateModeOverride() + { + if (!_forceAlternateModeOnLaunch) + return false; + + _forceAlternateModeOnLaunch = false; + return true; + } + + public static bool TryConsumeRedirectedAlternateModeOverride() + { + try + { + if (_forceAlternateModeSignalHandle != null) + { + return _forceAlternateModeSignalHandle.WaitOne(0); + } + + if (!EventWaitHandle.TryOpenExisting(ForceAlternateModeSignalEventName, out EventWaitHandle? signal)) + return false; + + using (signal) + { + return signal.WaitOne(0); + } + } + catch + { + return false; + } + } + + private static bool IsCtrlKeyDown() => (GetAsyncKeyState(VkControl) & 0x8000) != 0; + + private static void EnsureAlternateModeOverrideSignalHandle() + { + if (_forceAlternateModeSignalHandle != null) + return; + + try + { + _forceAlternateModeSignalHandle = new EventWaitHandle(false, EventResetMode.AutoReset, ForceAlternateModeSignalEventName); + } + catch + { + _forceAlternateModeSignalHandle = null; + } + } + + private static void SignalForceAlternateMode() + { + try + { + if (EventWaitHandle.TryOpenExisting(ForceAlternateModeSignalEventName, out EventWaitHandle? signal)) + { + using (signal) + { + signal.Set(); + } + + return; + } + + using EventWaitHandle fallbackSignal = new(false, EventResetMode.AutoReset, ForceAlternateModeSignalEventName); + fallbackSignal.Set(); + } + catch + { + // Ignore signaling failures and continue with normal activation redirection. + } + } + // Do the redirection on another thread, and use a non-blocking // wait method to wait for the redirection to complete. public static void RedirectActivationTo(AppActivationArguments args, diff --git a/Wino.Mail.WinUI/Services/NotificationBuilder.cs b/Wino.Mail.WinUI/Services/NotificationBuilder.cs index 315a833d..8c959082 100644 --- a/Wino.Mail.WinUI/Services/NotificationBuilder.cs +++ b/Wino.Mail.WinUI/Services/NotificationBuilder.cs @@ -21,7 +21,6 @@ namespace Wino.Mail.WinUI.Services; public class NotificationBuilder : INotificationBuilder { private const string MailApplicationId = "App"; - private const string CalendarApplicationId = "CalendarApp"; private readonly IAccountService _accountService; private readonly IFolderService _folderService; @@ -326,8 +325,8 @@ public class NotificationBuilder : INotificationBuilder private static string GetAppUserModelId(ToastTargetApp targetApp) { - var appId = targetApp == ToastTargetApp.Mail ? MailApplicationId : CalendarApplicationId; - return $"{Package.Current.Id.FamilyName}!{appId}"; + _ = targetApp; + return $"{Package.Current.Id.FamilyName}!{MailApplicationId}"; } private enum ToastTargetApp