diff --git a/src/Notepads/Core/NotepadsCore.cs b/src/Notepads/Core/NotepadsCore.cs index 1c1f5744d..79a632c32 100644 --- a/src/Notepads/Core/NotepadsCore.cs +++ b/src/Notepads/Core/NotepadsCore.cs @@ -814,6 +814,8 @@ private void Sets_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEven private async void Sets_SetDraggedOutside(object sender, SetDraggedOutsideEventArgs e) { + if (App.IsGameBarWidget) return; + if (Sets.Items?.Count > 1 && e.Set.Content is ITextEditor textEditor) { // Only allow untitled empty document to be dragged outside for now diff --git a/src/Notepads/Extensions/DispatcherExtensions.cs b/src/Notepads/Extensions/DispatcherExtensions.cs index 93bceb447..9081f3826 100644 --- a/src/Notepads/Extensions/DispatcherExtensions.cs +++ b/src/Notepads/Extensions/DispatcherExtensions.cs @@ -10,5 +10,28 @@ public static async Task CallOnUIThreadAsync(this CoreDispatcher dispatcher, Dis { await dispatcher.RunAsync(CoreDispatcherPriority.Normal, handler); } + + public static async Task RunTaskAsync(this CoreDispatcher dispatcher, + Func> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(priority, async () => + { + try + { + taskCompletionSource.SetResult(await func()); + } + catch (Exception ex) + { + taskCompletionSource.SetException(ex); + } + }); + return await taskCompletionSource.Task; + } + + // There is no TaskCompletionSource so we use a bool that we throw away. + public static async Task RunTaskAsync(this CoreDispatcher dispatcher, + Func func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal) => + await RunTaskAsync(dispatcher, async () => { await func(); return false; }, priority); } } \ No newline at end of file diff --git a/src/Notepads/Notepads.csproj b/src/Notepads/Notepads.csproj index cdfb31b7c..bd55bd99d 100644 --- a/src/Notepads/Notepads.csproj +++ b/src/Notepads/Notepads.csproj @@ -225,6 +225,7 @@ + @@ -604,6 +605,9 @@ 3.2.2 + + 5.3.200522001-prerelease + 6.2.10 diff --git a/src/Notepads/Package.appxmanifest b/src/Notepads/Package.appxmanifest index 08bc00619..f62ab318b 100644 --- a/src/Notepads/Package.appxmanifest +++ b/src/Notepads/Package.appxmanifest @@ -8,7 +8,8 @@ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:iot2="http://schemas.microsoft.com/appx/manifest/iot/windows10/2" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" - IgnorableNamespaces="uap mp uap5 desktop4 iot2 rescap"> + xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" + IgnorableNamespaces="uap mp uap5 desktop4 iot2 rescap uap3"> + + + + + + + + + + + + + + + + + + + true + true + true + + + + 400 + 400 + 400 + 400 + 1000 + 1000 + + + true + true + + + + + + + + + + + + + 600 + 395 + + + false + true + + + + + + + + + + + + + Microsoft.Gaming.XboxGameBar.winmd + + + + + + + + + + + + + + diff --git a/src/Notepads/Program.cs b/src/Notepads/Program.cs index 1b8a51a61..dc6af3023 100644 --- a/src/Notepads/Program.cs +++ b/src/Notepads/Program.cs @@ -20,14 +20,14 @@ static void Main(string[] args) 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.IsFirstInstance = true; - // Windows.UI.Xaml.Application.Start(p => new App()); - //} + 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.IsFirstInstance = true; + Windows.UI.Xaml.Application.Start(p => new App()); + } var instances = AppInstance.GetInstances(); diff --git a/src/Notepads/Services/ActivationService.cs b/src/Notepads/Services/ActivationService.cs index dcb8fbea6..a500a4dc8 100644 --- a/src/Notepads/Services/ActivationService.cs +++ b/src/Notepads/Services/ActivationService.cs @@ -1,9 +1,12 @@ namespace Notepads.Services { using System.Threading.Tasks; + using Microsoft.Gaming.XboxGameBar; using Notepads.Utilities; using Notepads.Views.MainPage; + using Notepads.Views.Settings; using Windows.ApplicationModel.Activation; + using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; public static class ActivationService @@ -26,6 +29,10 @@ public static async Task ActivateAsync(Frame rootFrame, IActivatedEventArgs e) { LaunchActivated(rootFrame, launchActivatedEventArgs); } + else if (e is XboxGameBarWidgetActivatedEventArgs xboxGameBarWidgetActivatedEventArgs) + { + GameBarActivated(rootFrame, xboxGameBarWidgetActivatedEventArgs); + } else // For other types of activated events { if (rootFrame.Content == null) @@ -100,5 +107,27 @@ private static async Task CommandActivated(Frame rootFrame, CommandLineActivated } } } + + public static void GameBarActivated(Frame rootFrame, XboxGameBarWidgetActivatedEventArgs xboxGameBarWidgetActivatedEventArgs) + { + LoggingService.LogInfo($"[{nameof(ActivationService)}] [XboxGameBarWidgetActivated] AppExtensionId: {xboxGameBarWidgetActivatedEventArgs.AppExtensionId}"); + + if (xboxGameBarWidgetActivatedEventArgs.IsLaunchActivation) + { + var xboxGameBarWidget = new XboxGameBarWidget( + xboxGameBarWidgetActivatedEventArgs, + Window.Current.CoreWindow, + rootFrame); + + if (xboxGameBarWidgetActivatedEventArgs.AppExtensionId == "Notepads") + { + rootFrame.Navigate(typeof(NotepadsMainPage), xboxGameBarWidget); + } + else if (xboxGameBarWidgetActivatedEventArgs.AppExtensionId == "NotepadsSettings") + { + rootFrame.Navigate(typeof(SettingsPage), xboxGameBarWidget); + } + } + } } } \ No newline at end of file diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs index 81d0e8b2f..0278bbc2a 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; + using Windows.Foundation; using Windows.Graphics.Printing; using Windows.Storage; using Notepads.Controls.Dialog; @@ -22,7 +23,7 @@ private async Task OpenNewFiles() try { - files = await FilePickerFactory.GetFileOpenPicker().PickMultipleFilesAsync(); + files = await OpenFilesUsingFileOpenPicker(); } catch (Exception ex) { @@ -47,6 +48,36 @@ private async Task OpenNewFiles() } } + private async Task> OpenFilesUsingFileOpenPicker() + { + IReadOnlyList files = null; + IAsyncOperation> filePickerOperation = null; + + var filePickerOperationDelegate = new Func(async () => + { + filePickerOperation = FilePickerFactory.GetFileOpenPicker().PickMultipleFilesAsync(); + files = await filePickerOperation; + }); + + if (App.IsGameBarWidget && _widget != null) + { + var foregroundWorkWrapper = new XboxGameBarForegroundWorkerWrapper(_widget, Dispatcher, filePickerOperationDelegate); + + foregroundWorkWrapper.CancelOperationRequested += (sender, eventArgs) => + { + filePickerOperation.Cancel(); + }; + + await foregroundWorkWrapper.ExecuteAsync(); + } + else + { + await filePickerOperationDelegate.Invoke(); + } + + return files; + } + public async Task OpenFile(StorageFile file, bool rebuildOpenRecentItems = true) { try @@ -134,14 +165,6 @@ public async Task OpenFiles(IReadOnlyList storageItems) return successCount; } - private async Task OpenFileUsingFileSavePicker(ITextEditor textEditor) - { - NotepadsCore.SwitchTo(textEditor); - StorageFile file = await FilePickerFactory.GetFileSavePicker(textEditor).PickSaveFileAsync(); - NotepadsCore.FocusOnTextEditor(textEditor); - return file; - } - private async Task SaveInternal(ITextEditor textEditor, StorageFile file, bool rebuildOpenRecentItems) { await NotepadsCore.SaveContentToFileAndUpdateEditorState(textEditor, file); @@ -167,7 +190,7 @@ private async Task Save(ITextEditor textEditor, bool saveAs, bool ignoreUn { if (textEditor.EditingFile == null || saveAs) { - file = await OpenFileUsingFileSavePicker(textEditor); + file = await PickFileForSavingUsingFileSavePicker(textEditor); if (file == null) return false; // User cancelled } else @@ -191,7 +214,7 @@ private async Task Save(ITextEditor textEditor, bool saveAs, bool ignoreUn if (promptSaveAs) { - file = await OpenFileUsingFileSavePicker(textEditor); + file = await PickFileForSavingUsingFileSavePicker(textEditor); if (file == null) return false; // User cancelled await SaveInternal(textEditor, file, rebuildOpenRecentItems); @@ -212,6 +235,40 @@ private async Task Save(ITextEditor textEditor, bool saveAs, bool ignoreUn } } + private async Task PickFileForSavingUsingFileSavePicker(ITextEditor textEditor) + { + NotepadsCore.SwitchTo(textEditor); + + StorageFile file = null; + IAsyncOperation filePickerOperation = null; + + var filePickerOperationDelegate = new Func(async () => + { + filePickerOperation = FilePickerFactory.GetFileSavePicker(textEditor).PickSaveFileAsync(); + file = await filePickerOperation; + }); + + if (App.IsGameBarWidget && _widget != null) + { + var foregroundWorkWrapper = new XboxGameBarForegroundWorkerWrapper(_widget, Dispatcher, filePickerOperationDelegate); + + foregroundWorkWrapper.CancelOperationRequested += (sender, eventArgs) => + { + filePickerOperation.Cancel(); + }; + + await foregroundWorkWrapper.ExecuteAsync(); + } + else + { + await filePickerOperationDelegate.Invoke(); + } + + NotepadsCore.FocusOnTextEditor(textEditor); + + return file; + } + private async Task SaveAll(ITextEditor[] textEditors) { var success = false; diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs index 7b02764de..a828556ea 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs @@ -26,6 +26,7 @@ using Windows.UI.Xaml.Media.Animation; using Windows.UI.Xaml.Navigation; using Microsoft.AppCenter.Analytics; + using Microsoft.Gaming.XboxGameBar; using Windows.Graphics.Printing; public sealed partial class NotepadsMainPage : Page @@ -43,6 +44,9 @@ public sealed partial class NotepadsMainPage : Page private INotepadsCore _notepadsCore; + private XboxGameBarWidget _widget; // maintain throughout the lifetime of the notepads game bar widget + private XboxGameBarWidgetControl _widgetControl; + private INotepadsCore NotepadsCore { get @@ -191,6 +195,17 @@ protected override void OnNavigatedTo(NavigationEventArgs e) case ProtocolActivatedEventArgs protocol: _appLaunchUri = protocol.Uri; break; + case XboxGameBarWidget widget: + _widget = widget; + _widgetControl = new XboxGameBarWidgetControl(_widget); + _widget.SettingsClicked += Widget_SettingsClicked; + //_widget.PinnedChanged += Widget_PinnedChanged; + //_widget.FavoritedChanged += Widget_FavoritedChanged; + //_widget.RequestedThemeChanged += Widget_RequestedThemeChanged; + //_widget.WindowStateChanged += Widget_WindowStateChanged; + //_widget.GameBarDisplayModeChanged += Widget_GameBarDisplayModeChanged; + Window.Current.Closed += Widget_MainWindowClosed; + break; } } @@ -274,6 +289,14 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) Window.Current.CoreWindow.Activated -= CoreWindow_Activated; Window.Current.CoreWindow.Activated += CoreWindow_Activated; } + else + { + if (_widget != null) + { + _widget.VisibleChanged -= Widget_VisibleChanged; + _widget.VisibleChanged += Widget_VisibleChanged; + } + } } private async void App_EnteredBackground(object sender, Windows.ApplicationModel.EnteredBackgroundEventArgs e) @@ -451,6 +474,49 @@ private static void UpdateApplicationTitle(ITextEditor activeTextEditor) #endregion + #region XboxGameBar + + private async void Widget_SettingsClicked(XboxGameBarWidget sender, object args) + { + await _widget.ActivateSettingsAsync(); + } + + private async void Widget_VisibleChanged(XboxGameBarWidget sender, object args) + { + if (sender.Visible) + { + LoggingService.LogInfo($"[{nameof(NotepadsMainPage)}] Game Bar Widget Visibility Changed, Visible = {sender.Visible}.", consoleOnly: true); + NotepadsCore.GetSelectedTextEditor()?.StartCheckingFileStatusPeriodically(); + if (AppSettingsService.IsSessionSnapshotEnabled) + { + SessionManager.StartSessionBackup(); + } + } + else + { + LoggingService.LogInfo($"[{nameof(NotepadsMainPage)}] Game Bar Widget Visibility Changed, Visible = {sender.Visible}.", consoleOnly: true); + NotepadsCore.GetSelectedTextEditor()?.StopCheckingFileStatus(); + if (AppSettingsService.IsSessionSnapshotEnabled) + { + await SessionManager.SaveSessionAsync(); + SessionManager.StopSessionBackup(); + } + } + } + + private void Widget_MainWindowClosed(object sender, Windows.UI.Core.CoreWindowEventArgs e) + { + // Un-registering events + Window.Current.Closed -= Widget_MainWindowClosed; + _widget.SettingsClicked -= Widget_SettingsClicked; + + // Cleanup game bar objects + _widget = null; + _widgetControl = null; + } + + #endregion + #region NotepadsCore Events private void OnTextEditorLoaded(object sender, ITextEditor textEditor) diff --git a/src/Notepads/Views/Settings/SettingsPage.xaml.cs b/src/Notepads/Views/Settings/SettingsPage.xaml.cs index 82d51f292..428b6d875 100644 --- a/src/Notepads/Views/Settings/SettingsPage.xaml.cs +++ b/src/Notepads/Views/Settings/SettingsPage.xaml.cs @@ -1,5 +1,6 @@ namespace Notepads.Views.Settings { + using Microsoft.Gaming.XboxGameBar; using Notepads.Extensions; using Notepads.Services; using System.Linq; @@ -10,6 +11,8 @@ public sealed partial class SettingsPage : Page { + private XboxGameBarWidget _widget; // maintain throughout the lifetime of the settings game bar widget + public SettingsPage() { InitializeComponent(); @@ -62,9 +65,21 @@ protected override void OnNavigatedTo(NavigationEventArgs e) { case null: return; + case XboxGameBarWidget widget: + _widget = widget; + Window.Current.Closed += WidgetSettingsWindowClosed; + break; } } + private void WidgetSettingsWindowClosed(object sender, Windows.UI.Core.CoreWindowEventArgs e) + { + // Un-registering events + Window.Current.Closed -= WidgetSettingsWindowClosed; + // Cleanup game bar objects + _widget = null; + } + private void SettingsPanel_OnItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) { SettingsPanel.Show((args.InvokedItem as string), (args.InvokedItemContainer as NavigationViewItem)?.Tag as string); diff --git a/src/Notepads/Widget/XboxGameBarForegroundWorkerWrapper.cs b/src/Notepads/Widget/XboxGameBarForegroundWorkerWrapper.cs new file mode 100644 index 000000000..22a2252d3 --- /dev/null +++ b/src/Notepads/Widget/XboxGameBarForegroundWorkerWrapper.cs @@ -0,0 +1,73 @@ +namespace Notepads +{ + using System; + using System.Threading.Tasks; + using Windows.UI.Core; + using Notepads.Extensions; + using Microsoft.Gaming.XboxGameBar.Restricted; + using Microsoft.Gaming.XboxGameBar; + + public class XboxGameBarForegroundWorkerWrapper + { + XboxGameBarWidget _widget; + private Func _func; + private CoreDispatcher _dispatcher; + + public event EventHandler CancelOperationRequested; + + public XboxGameBarForegroundWorkerWrapper( + XboxGameBarWidget widget, + CoreDispatcher dispatcher, + Func foregroundWorkFunc) + { + _widget = widget; + _func = foregroundWorkFunc; + _dispatcher = dispatcher; + } + + public async Task ExecuteAsync() + { + // Create a lambda for the UI work and re-use if not running as a Game Bar widget + // If you are doing async work on the UI thread inside this lambda, it must be awaited before the lambda returns to ensure Game Bar is + // in the right state for the entirety of the foreground operation. + // We recommend using the Dispatcher RunTaskAsync task extension to make this easier + // Look at Extensions/DispatcherTaskExtensions.cs + // For more information you can read this blog post: https://devblogs.microsoft.com/oldnewthing/20190327-00/?p=102364 + // For another approach more akin to how C++/WinRT handles awaitable thread switching, read this blog post: https://devblogs.microsoft.com/oldnewthing/20190328-00/?p=102368 + ForegroundWorkHandler foregroundWorkHandler = () => + { + var mainTask = Task.Run(async () => + { + await _dispatcher.RunTaskAsync(async () => + { + await _func.Invoke(); + }); + }); + + var continueTask = mainTask.ContinueWith(t => + { + if (t.IsFaulted) + { + // Throw the inner exception if it's there, otherwise just throw the outer one + throw t.Exception.InnerException ?? t.Exception; + } + + return true; + }).AsAsyncOperation(); + + return continueTask; + }; + + var foregroundWorker = new XboxGameBarForegroundWorker(_widget, foregroundWorkHandler); + + foregroundWorker.CancelOperationRequested += CancelOperationRequestedHandler; + await foregroundWorker.ExecuteAsync(); + foregroundWorker.CancelOperationRequested -= CancelOperationRequestedHandler; + } + + private void CancelOperationRequestedHandler(XboxGameBarForegroundWorker sender, object args) + { + CancelOperationRequested?.Invoke(this, EventArgs.Empty); + } + } +}