diff --git a/src/Notepads/App.xaml.cs b/src/Notepads/App.xaml.cs index c56ee5d4b..82a488a1f 100644 --- a/src/Notepads/App.xaml.cs +++ b/src/Notepads/App.xaml.cs @@ -28,13 +28,28 @@ sealed partial class App : Application public static Guid Id { get; } = Guid.NewGuid(); - public static bool IsPrimaryInstance = false; + public static event EventHandler OnInstanceTypeChanged; + + private static bool _isPrimaryInstance = false; + public static bool IsPrimaryInstance + { + get => _isPrimaryInstance; + set + { + if (value != _isPrimaryInstance) + { + _isPrimaryInstance = value; + OnInstanceTypeChanged?.Invoke(null, value); + } + } + } + public static bool IsGameBarWidget = false; // Notepads GitHub CD workflow will swap null with production value getting from Github Secrets private const string AppCenterSecret = null; - public static Mutex InstanceHandlerMutex { get; set; } + private static Mutex InstanceHandlerMutex = null; /// /// Initializes the singleton application object. This is the first line of authored code @@ -48,19 +63,6 @@ public App() var services = new Type[] { typeof(Crashes), typeof(Analytics) }; AppCenter.Start(AppCenterSecret, services); - InstanceHandlerMutex = new Mutex(true, App.ApplicationName, out bool isNew); - if (isNew) - { - IsPrimaryInstance = true; - ApplicationSettingsStore.Write(SettingsKey.ActiveInstanceIdStr, null); - } - else - { - InstanceHandlerMutex.Close(); - } - - LoggingService.LogInfo($"[{nameof(App)}] Started: Instance = {Id} IsPrimaryInstance: {IsPrimaryInstance} IsGameBarWidget: {IsGameBarWidget}."); - ApplicationSettingsStore.Write(SettingsKey.ActiveInstanceIdStr, App.Id.ToString()); InitializeComponent(); @@ -76,6 +78,7 @@ public App() protected override async void OnLaunched(LaunchActivatedEventArgs e) { await ActivateAsync(e); + base.OnLaunched(e); } protected override async void OnFileActivated(FileActivatedEventArgs args) @@ -92,6 +95,11 @@ protected override async void OnActivated(IActivatedEventArgs args) private async Task ActivateAsync(IActivatedEventArgs e) { + if (!(e is LaunchActivatedEventArgs args && args.PrelaunchActivated)) + { + InitializeInstance(); + } + bool rootFrameCreated = false; if (!(Window.Current.Content is Frame rootFrame)) @@ -291,6 +299,34 @@ private static void ExtendViewIntoTitleBar() } } + public static void InitializeInstance() + { + if (InstanceHandlerMutex == null) + { + InstanceHandlerMutex = new Mutex(true, App.ApplicationName, out bool createdNew); + + if (createdNew) + { + IsPrimaryInstance = true; + ApplicationSettingsStore.Write(SettingsKey.ActiveInstanceIdStr, null); + } + else + { + InstanceHandlerMutex?.Close(); + } + + LoggingService.LogInfo( + $"[{nameof(App)}] Started: Instance = {Id} " + + $"IsPrimaryInstance: {IsPrimaryInstance} " + + $"IsGameBarWidget: {IsGameBarWidget}."); + } + } + + public static void Dispose() + { + InstanceHandlerMutex?.Dispose(); + } + //private static void UpdateAppVersion() //{ // var packageVer = Package.Current.Id.Version; diff --git a/src/Notepads/Core/NotepadsCore.cs b/src/Notepads/Core/NotepadsCore.cs index 00a81bd3f..663a96780 100644 --- a/src/Notepads/Core/NotepadsCore.cs +++ b/src/Notepads/Core/NotepadsCore.cs @@ -149,8 +149,20 @@ public void OpenTextEditors(ITextEditor[] editors, Guid? selectedEditorId = null { bool selectedEditorFound = false; + // Notepads should replace current "Untitled.txt" with open file if it is empty and it is the only tab that has been created. + if (GetNumberOfOpenedTextEditors() == 1 && editors.Length > 0) + { + var selectedEditor = GetAllTextEditors().First(); + if (selectedEditor.EditingFile == null && !selectedEditor.IsModified) + { + Sets.Items?.Clear(); + } + } + foreach (var textEditor in editors) { + Sets.Items?.Remove(GetTextEditorSetsViewItem(textEditor.EditingFile)); + var editorSetsViewItem = CreateTextEditorSetsViewItem(textEditor); Sets.Items?.Add(editorSetsViewItem); if (selectedEditorId.HasValue && textEditor.Id == selectedEditorId.Value) @@ -429,7 +441,7 @@ private SetsViewItem CreateTextEditorSetsViewItem(ITextEditor textEditor) private SetsViewItem GetTextEditorSetsViewItem(StorageFile file) { - if (Sets.Items == null) return null; + if (Sets.Items == null || file == null) return null; foreach (SetsViewItem setsItem in Sets.Items) { if (!(setsItem.Content is ITextEditor textEditor)) continue; diff --git a/src/Notepads/Program.cs b/src/Notepads/Program.cs index 7a9e0fa67..cd426c7f6 100644 --- a/src/Notepads/Program.cs +++ b/src/Notepads/Program.cs @@ -16,60 +16,51 @@ static void Main(string[] args) Task.Run(LoggingService.InitializeFileSystemLoggingAsync); #endif - IActivatedEventArgs activatedArgs = AppInstance.GetActivatedEventArgs(); - - //if (activatedArgs == null) - //{ - // // No activated event args, so this is not an activation via the multi-instance ID - // // Just create a new instance and let App OnActivated resolve the launch - // App.IsGameBarWidget = true; - // App.IsPrimaryInstance = true; - // Windows.UI.Xaml.Application.Start(p => new App()); - //} - - if (activatedArgs is FileActivatedEventArgs) - { - RedirectOrCreateNewInstance(); - } - else if (activatedArgs is CommandLineActivatedEventArgs) + switch (AppInstance.GetActivatedEventArgs()) { - RedirectOrCreateNewInstance(); - } - else if (activatedArgs is ProtocolActivatedEventArgs protocolActivatedEventArgs) - { - LoggingService.LogInfo($"[{nameof(Main)}] [ProtocolActivated] Protocol: {protocolActivatedEventArgs.Uri}"); - var protocol = NotepadsProtocolService.GetOperationProtocol(protocolActivatedEventArgs.Uri, out _); - if (protocol == NotepadsOperationProtocol.OpenNewInstance) - { - OpenNewInstance(); - } - else - { + case FileActivatedEventArgs _: + case CommandLineActivatedEventArgs _: RedirectOrCreateNewInstance(); - } - } - else if (activatedArgs is LaunchActivatedEventArgs launchActivatedEventArgs) - { - bool handled = false; - - if (!string.IsNullOrEmpty(launchActivatedEventArgs.Arguments)) - { - var protocol = NotepadsProtocolService.GetOperationProtocol(new Uri(launchActivatedEventArgs.Arguments), out _); + break; + case ProtocolActivatedEventArgs protocolActivatedEventArgs: + LoggingService.LogInfo($"[{nameof(Main)}] [ProtocolActivated] Protocol: {protocolActivatedEventArgs.Uri}"); + var protocol = NotepadsProtocolService.GetOperationProtocol(protocolActivatedEventArgs.Uri, out _); if (protocol == NotepadsOperationProtocol.OpenNewInstance) { - handled = true; OpenNewInstance(); } - } + else + { + RedirectOrCreateNewInstance(); + } + break; + case LaunchActivatedEventArgs launchActivatedEventArgs: + bool handled = false; + if (!string.IsNullOrEmpty(launchActivatedEventArgs.Arguments)) + { + protocol = NotepadsProtocolService.GetOperationProtocol(new Uri(launchActivatedEventArgs.Arguments), out _); + if (protocol == NotepadsOperationProtocol.OpenNewInstance) + { + handled = true; + OpenNewInstance(); + } + } - if (!handled) - { + if (!handled) + { + RedirectOrCreateNewInstance(); + } + break; + //case null: + // // No activated event args, so this is not an activation via the multi-instance ID + // // Just create a new instance and let App OnActivated resolve the launch + // App.IsGameBarWidget = true; + // App.IsPrimaryInstance = true; + // Windows.UI.Xaml.Application.Start(p => new App()); + // break; + default: RedirectOrCreateNewInstance(); - } - } - else - { - RedirectOrCreateNewInstance(); + break; } } @@ -105,13 +96,12 @@ private static AppInstance GetLastActiveInstance() { var instances = AppInstance.GetInstances(); - if (instances.Count == 0) - { - return null; - } - else if (instances.Count == 1) + switch (instances.Count) { - return instances.FirstOrDefault(); + case 0: + return null; + case 1: + return instances.FirstOrDefault(); } if (!(ApplicationSettingsStore.Read(SettingsKey.ActiveInstanceIdStr) is string activeInstance)) diff --git a/src/Notepads/Services/ActivationService.cs b/src/Notepads/Services/ActivationService.cs index dcb8fbea6..7fe58112a 100644 --- a/src/Notepads/Services/ActivationService.cs +++ b/src/Notepads/Services/ActivationService.cs @@ -10,28 +10,26 @@ public static class ActivationService { public static async Task ActivateAsync(Frame rootFrame, IActivatedEventArgs e) { - if (e is ProtocolActivatedEventArgs protocolActivatedEventArgs) + switch (e) { - ProtocolActivated(rootFrame, protocolActivatedEventArgs); - } - else if (e is FileActivatedEventArgs fileActivatedEventArgs) - { - await FileActivated(rootFrame, fileActivatedEventArgs); - } - else if (e is CommandLineActivatedEventArgs commandLineActivatedEventArgs) - { - await CommandActivated(rootFrame, commandLineActivatedEventArgs); - } - else if (e is LaunchActivatedEventArgs launchActivatedEventArgs) - { - LaunchActivated(rootFrame, launchActivatedEventArgs); - } - else // For other types of activated events - { - if (rootFrame.Content == null) - { - rootFrame.Navigate(typeof(NotepadsMainPage)); - } + case ProtocolActivatedEventArgs protocolActivatedEventArgs: + ProtocolActivated(rootFrame, protocolActivatedEventArgs); + break; + case FileActivatedEventArgs fileActivatedEventArgs: + await FileActivated(rootFrame, fileActivatedEventArgs); + break; + case CommandLineActivatedEventArgs commandLineActivatedEventArgs: + await CommandActivated(rootFrame, commandLineActivatedEventArgs); + break; + case LaunchActivatedEventArgs launchActivatedEventArgs: + LaunchActivated(rootFrame, launchActivatedEventArgs); + break; + default: // For other types of activated events + if (rootFrame.Content == null) + { + rootFrame.Navigate(typeof(NotepadsMainPage)); + } + break; } } diff --git a/src/Notepads/Services/AppSettingsService.cs b/src/Notepads/Services/AppSettingsService.cs index eb7bab0aa..417b5f724 100644 --- a/src/Notepads/Services/AppSettingsService.cs +++ b/src/Notepads/Services/AppSettingsService.cs @@ -20,6 +20,7 @@ public static class AppSettingsService public static event EventHandler OnDefaultEncodingChanged; public static event EventHandler OnDefaultTabIndentsChanged; public static event EventHandler OnStatusBarVisibilityChanged; + public static event EventHandler OnSessionBackupAndRestoreOptionForInstanceChanged; public static event EventHandler OnSessionBackupAndRestoreOptionChanged; public static event EventHandler OnHighlightMisspelledWordsChanged; @@ -328,23 +329,47 @@ private static void InitializeStatusBarSettings() private static void InitializeSessionSnapshotSettings() { // We should disable session snapshot feature on multi instances + App.OnInstanceTypeChanged += (_, args) => + { + var wasSessionSnapshotEnabled = _isSessionSnapshotEnabled; + + _isSessionSnapshotEnabled = IsSessionSnapshotEnabledInternal(); + + if (wasSessionSnapshotEnabled != _isSessionSnapshotEnabled) + { + if (_isSessionSnapshotEnabled) + { + OnSessionBackupAndRestoreOptionForInstanceChanged?.Invoke(null, _isSessionSnapshotEnabled); + } + else + { + OnSessionBackupAndRestoreOptionChanged?.Invoke(null, _isSessionSnapshotEnabled); + } + } + }; + + _isSessionSnapshotEnabled = IsSessionSnapshotEnabledInternal(); + } + + private static bool IsSessionSnapshotEnabledInternal() + { if (!App.IsPrimaryInstance) { - _isSessionSnapshotEnabled = false; + return false; } else if (App.IsGameBarWidget) { - _isSessionSnapshotEnabled = true; + return true; } else { if (ApplicationSettingsStore.Read(SettingsKey.EditorEnableSessionBackupAndRestoreBool) is bool enableSessionBackupAndRestore) { - _isSessionSnapshotEnabled = enableSessionBackupAndRestore; + return enableSessionBackupAndRestore; } else { - _isSessionSnapshotEnabled = false; + return false; } } } diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs index df903eb93..8d6445c0a 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs @@ -9,7 +9,7 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; - using Windows.UI.Xaml.Media; + using Notepads.Extensions; using Notepads.Services; public sealed partial class NotepadsMainPage @@ -32,11 +32,9 @@ private void InitializeMainMenu() MenuPrintAllButton.Click += async (sender, args) => await PrintAll(NotepadsCore.GetAllTextEditors()); MenuSettingsButton.Click += (sender, args) => RootSplitView.IsPaneOpen = true; - if (!App.IsPrimaryInstance) - { - MainMenuButton.Foreground = new SolidColorBrush(ThemeSettingsService.AppAccentColor); - MenuSettingsButton.IsEnabled = false; - } + App.OnInstanceTypeChanged += async (sender, args) => await Dispatcher.CallOnUIThreadAsync( + () => CustomizeBasedOnInstanceType(args)); + CustomizeBasedOnInstanceType(App.IsPrimaryInstance); if (App.IsGameBarWidget) { @@ -61,6 +59,19 @@ private void InitializeMainMenu() MainMenuButtonFlyout.Opening += MainMenuButtonFlyout_Opening; } + private void CustomizeBasedOnInstanceType(bool isPrimaryInstance) + { + // Apply UI changes for shadow instance + if (!isPrimaryInstance) + { + VisualStateManager.GoToState(this, "ShadowInstance", true); + } + else + { + VisualStateManager.GoToState(this, "PrimaryInstance", true); + } + } + private void MainMenuButtonFlyout_Opening(object sender, object e) { var selectedTextEditor = NotepadsCore.GetSelectedTextEditor(); diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.StatusBar.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.StatusBar.cs index 1295dcd3b..75606729f 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.StatusBar.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.StatusBar.cs @@ -25,6 +25,8 @@ private void InitializeStatusBar() { ShowHideStatusBar(AppSettingsService.ShowStatusBar); AppSettingsService.OnStatusBarVisibilityChanged += OnStatusBarVisibilityChanged; + App.OnInstanceTypeChanged += async (_, args) => await Dispatcher.CallOnUIThreadAsync( + () => UpdateShadowWindowIndicator()); } private void SetupStatusBar(ITextEditor textEditor) diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml index 4ea5550b2..1850109a7 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml @@ -13,6 +13,20 @@ Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + + + + + + + + + + + + diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs index 7badfc983..906d37e84 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs @@ -27,6 +27,7 @@ using Windows.UI.Xaml.Navigation; using Microsoft.AppCenter.Analytics; using Windows.Graphics.Printing; + //using System.Threading; public sealed partial class NotepadsMainPage : Page { @@ -38,6 +39,7 @@ public sealed partial class NotepadsMainPage : Page private readonly ResourceLoader _resourceLoader = ResourceLoader.GetForCurrentView(); + private bool _sessionRestoreCompleted = false; private bool _loaded = false; private bool _appShouldExitAfterLastEditorClosed = false; @@ -97,6 +99,7 @@ public NotepadsMainPage() InitializeKeyboardShortcuts(); // Session backup and restore toggle + AppSettingsService.OnSessionBackupAndRestoreOptionForInstanceChanged += OnSessionBackupAndRestoreOptionForInstanceChanged; AppSettingsService.OnSessionBackupAndRestoreOptionChanged += OnSessionBackupAndRestoreOptionChanged; // Register for printing @@ -116,7 +119,7 @@ public NotepadsMainPage() else { Window.Current.SizeChanged += WindowSizeChanged; - Window.Current.VisibilityChanged += WindowVisibilityChangedEventHandler; + Window.Current.VisibilityChanged += WindowVisibilityChanged; } } @@ -170,7 +173,7 @@ private static async Task OpenNewAppInstance() } } - #region Application Life Cycle & Window management + #region Application Life Cycle & Window management // Handles external links or cmd args activation before Sets loaded protected override void OnNavigatedTo(NavigationEventArgs e) @@ -198,9 +201,10 @@ protected override void OnNavigatedTo(NavigationEventArgs e) // Open files from external links or cmd args on Sets Loaded private async void Sets_Loaded(object sender, RoutedEventArgs e) { - int loadedCount = 0; + var loadedCount = 0; + var isSessionSnapshotEnabled = AppSettingsService.IsSessionSnapshotEnabled; - if (!_loaded && AppSettingsService.IsSessionSnapshotEnabled) + if (!_loaded && isSessionSnapshotEnabled) { try { @@ -213,6 +217,8 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) } } + _sessionRestoreCompleted = true; + if (_appLaunchFiles != null && _appLaunchFiles.Count > 0) { loadedCount += await OpenFiles(_appLaunchFiles); @@ -245,14 +251,14 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) NotepadsCore.OpenNewTextEditor(_defaultNewFileName); } - if (!App.IsPrimaryInstance) - { - NotificationCenter.Instance.PostNotification(_resourceLoader.GetString("App_ShadowWindowIndicator_Description"), 4000); - } + App.OnInstanceTypeChanged -= ShowShadowWindowIndicatorBasedOnInstanceType; + App.OnInstanceTypeChanged += ShowShadowWindowIndicatorBasedOnInstanceType; + ShowShadowWindowIndicatorBasedOnInstanceType(this, App.IsPrimaryInstance); + _loaded = true; } - if (AppSettingsService.IsSessionSnapshotEnabled) + if (isSessionSnapshotEnabled) { SessionManager.IsBackupEnabled = true; SessionManager.StartSessionBackup(); @@ -265,7 +271,7 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) // An issue with the Game Bar extension model and Windows platform prevents the Notepads process from exiting cleanly // when more than one CoreWindow has been created, and NotepadsMainPage is the last to close. The common case for this // is to open Notepads in Game Bar, then open its settings, then close the settings and finally close Notepads. - // This puts the process in a bad state where it will no longer open in Game Bar and the Notepads process is orphaned. + // This puts the process in a bad state where it will no longer open in Game Bar and the Notepads process is orphaned. // To work around this do not use the EnteredBackground event when running as a widget. // Microsoft is tracking this issue as VSO#25735260 Application.Current.EnteredBackground -= App_EnteredBackground; @@ -276,6 +282,19 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) } } + private async void ShowShadowWindowIndicatorBasedOnInstanceType(object sender, bool isPrimaryInstance) + { + if (!isPrimaryInstance) + { + await Dispatcher.CallOnUIThreadAsync(() => + { + NotificationCenter.Instance.PostNotification( + _resourceLoader.GetString("App_ShadowWindowIndicator_Description"), + 4000); + }); + } + } + private async void App_EnteredBackground(object sender, Windows.ApplicationModel.EnteredBackgroundEventArgs e) { var deferral = e.GetDeferral(); @@ -317,8 +336,9 @@ private void CoreWindow_Activated(Windows.UI.Core.CoreWindow sender, Windows.UI. } } - private void WindowVisibilityChangedEventHandler(System.Object sender, Windows.UI.Core.VisibilityChangedEventArgs e) + private void WindowVisibilityChanged(System.Object sender, Windows.UI.Core.VisibilityChangedEventArgs e) { + App.InitializeInstance(); LoggingService.LogInfo($"[{nameof(NotepadsMainPage)}] Window Visibility Changed, Visible = {e.Visible}.", consoleOnly: true); // Perform operations that should take place when the application becomes visible rather than // when it is prelaunched, such as building a what's new feed @@ -349,14 +369,14 @@ private async void MainPage_CloseRequested(object sender, Windows.UI.Core.Previe { // Save session before app exit await SessionManager.SaveSessionAsync(() => { SessionManager.IsBackupEnabled = false; }); - App.InstanceHandlerMutex?.Dispose(); + App.Dispose(); deferral.Complete(); return; } if (!NotepadsCore.HaveUnsavedTextEditor()) { - App.InstanceHandlerMutex?.Dispose(); + App.Dispose(); deferral.Complete(); return; } @@ -389,14 +409,14 @@ private async void MainPage_CloseRequested(object sender, Windows.UI.Core.Previe } else { - App.InstanceHandlerMutex?.Dispose(); + App.Dispose(); } deferral.Complete(); }, discardAndExitAction: () => { - App.InstanceHandlerMutex?.Dispose(); + App.Dispose(); deferral.Complete(); }, cancelAction: () => @@ -430,6 +450,22 @@ private void HideAllOpenFlyouts() } } + private async void OnSessionBackupAndRestoreOptionForInstanceChanged(object sender, bool isSessionBackupAndRestoreEnabled) + { + // Execute only if session restore is complete before instance type initialized + if (_sessionRestoreCompleted) + { + if (isSessionBackupAndRestoreEnabled) + { + await Dispatcher.CallOnUIThreadAsync(async () => + { + await SessionManager.LoadLastSessionAsync(); + OnSessionBackupAndRestoreOptionChanged(sender, isSessionBackupAndRestoreEnabled); + }); + } + } + } + private async void OnSessionBackupAndRestoreOptionChanged(object sender, bool isSessionBackupAndRestoreEnabled) { await Dispatcher.CallOnUIThreadAsync(async () =>