Browse Source

Shutdown confirmation

Krzysztof Krysiński 1 year ago
parent
commit
63941c2409

+ 1 - 1
src/PixiEditor.AvaloniaUI/Helpers/Converters/BoolOrToVisibilityConverter.cs

@@ -14,6 +14,6 @@ internal class BoolOrToVisibilityConverter : SingleInstanceMultiValueConverter<B
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
         bool boolean = values.Aggregate(false, (acc, cur) => acc |= (cur as bool?) ?? false);
-        return boolean ? true : false;
+        return boolean;
     }
 }

+ 44 - 26
src/PixiEditor.AvaloniaUI/Initialization/ClassicDesktopEntry.cs

@@ -3,15 +3,18 @@ using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Threading;
+using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Controllers;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.ExceptionHandling;
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.AvaloniaUI.Views.Dialogs;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.OperatingSystem;
 using PixiEditor.Platform;
@@ -22,7 +25,7 @@ namespace PixiEditor.AvaloniaUI.Initialization;
 
 internal class ClassicDesktopEntry
 {
-        /// <summary>The event mutex name.</summary>
+    /// <summary>The event mutex name.</summary>
     private const string UniqueEventName = "33f1410b-2ad7-412a-a468-34fe0a85747c";
 
     /// <summary>The unique mutex name.</summary>
@@ -31,13 +34,16 @@ internal class ClassicDesktopEntry
     /// <summary>The event wait handle.</summary>
     private EventWaitHandle _eventWaitHandle;
 
-    private string passedArgsFile = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PixiEditor", ".passedArgs");
+    private string passedArgsFile = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+        "PixiEditor", ".passedArgs");
+
     private IClassicDesktopStyleApplicationLifetime desktop;
 
     public ClassicDesktopEntry(IClassicDesktopStyleApplicationLifetime desktop)
     {
         this.desktop = desktop;
         desktop.Startup += Start;
+        desktop.ShutdownRequested += ShutdownRequested;
     }
 
     /// <summary>The mutex.</summary>
@@ -68,17 +74,18 @@ internal class ClassicDesktopEntry
                     //MessageBox.Show("Fatal error", $"Fatal error while trying to open crash report in App.OnStartup()\n{exception}");
                 }
             }
+
             return;
         }
 
         Dispatcher dispatcher = Dispatcher.UIThread;
 
-        #if !STEAM
+#if !STEAM
         if (!HandleNewInstance(dispatcher))
         {
             return;
         }
-        #endif
+#endif
 
         InitOperatingSystem();
         var extensionLoader = InitApp();
@@ -93,16 +100,17 @@ internal class ClassicDesktopEntry
         IPlatform.RegisterPlatform(platform);
         platform.PerformHandshake();
     }
-    
+
     public ExtensionLoader InitApp()
     {
         InitPlatform();
 
         ExtensionLoader extensionLoader = new ExtensionLoader(Paths.ExtensionsFullPath);
         //TODO: fetch from extension store
-        extensionLoader.AddOfficialExtension("pixieditor.supporterpack", new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
+        extensionLoader.AddOfficialExtension("pixieditor.supporterpack",
+            new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
         extensionLoader.LoadExtensions();
-        
+
         return extensionLoader;
     }
 
@@ -154,7 +162,8 @@ internal class ClassicDesktopEntry
                                     List<string> args = new List<string>();
                                     if (File.Exists(passedArgsFile))
                                     {
-                                        args = CommandLineHelpers.SplitCommandLine(File.ReadAllText(passedArgsFile)).ToList();
+                                        args = CommandLineHelpers.SplitCommandLine(File.ReadAllText(passedArgsFile))
+                                            .ToList();
                                         File.Delete(passedArgsFile);
                                     }
 
@@ -203,24 +212,6 @@ internal class ClassicDesktopEntry
         return wrappedArgs;
     }
 
-    //TODO: Implement this
-    /*protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
-    {
-        base.OnSessionEnding(e);
-
-        var vm = ViewModelMain.Current;
-        if (vm is null)
-            return;
-
-        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
-        {
-            ConfirmationType confirmation = ConfirmationDialog.Show(
-                new LocalizedString("SESSION_UNSAVED_DATA", e.ReasonSessionEnding),
-                $"{e.ReasonSessionEnding}");
-            e.Cancel = confirmation != ConfirmationType.Yes;
-        }
-    }*/
-
     private bool ParseArgument(string pattern, string args, out Group[] groups)
     {
         Match match = Regex.Match(args, pattern, RegexOptions.IgnoreCase);
@@ -233,4 +224,31 @@ internal class ClassicDesktopEntry
 
         return match.Success;
     }
+
+    private void ShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
+    {
+        // TODO: Make sure this works
+        var vm = ViewModelMain.Current;
+        if (vm is null)
+            return;
+
+        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
+        {
+            e.Cancel = true;
+            Task.Run(async () =>
+            {
+                await Dispatcher.UIThread.InvokeAsync(async () =>
+                {
+                    ConfirmationType confirmation = await ConfirmationDialog.Show(
+                        new LocalizedString("SESSION_UNSAVED_DATA", "Shutdown"),
+                        $"Shutdown");
+
+                    if (confirmation != ConfirmationType.Yes)
+                    {
+                        desktop.Shutdown();
+                    }
+                });
+            });
+        }
+    }
 }

+ 0 - 20
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs

@@ -84,26 +84,6 @@ internal class ActionAccumulator
             }
             if (undoBoundaryPassed)
                 internals.Updater.AfterUndoBoundaryPassed();
-
-            // render changes
-            // If you are a sane person or maybe just someone who reads WPF documentation, you might think that the reasonable order of operations should be
-            // 1. Lock the writeable bitmaps
-            // 2. Update their contents
-            // 3. Add dirty rectangles
-            // 4. Unlock them
-            // As it turns out, doing operations in this order leads to WPF render thread crashing in some circumstatances.
-            // Then the whole app freezes without throwing any errors, because the UI thread is blocked on a mutex, waiting for the dead render thread.
-            // This is despite the order clearly being adviced in the documentations here: https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.imaging.writeablebitmap?view=windowsdesktop-6.0&viewFallbackFrom=net-6.0#remarks
-            // Because of that, I'm executing the operations in the order that makes a lot less sense:
-            // 1. Update the contents of the bitmaps
-            // 2. Lock Them
-            // 3. Add dirty rectangles
-            // 4. Unlock
-            // The locks clearly do nothing useful here, and I'm only calling them because WriteableBitmap checks if it's locked before letting you add dirty rectangles.
-            // Really, the locks are supposed to prevent me from updating the bitmap contents in step 1, but they can't since I'm doing direct unsafe memory copying
-            // Somehow this all works
-            // Also, there is a bug report for this on github https://github.com/dotnet/wpf/issues/5816
-
             
             // lock bitmaps
             foreach (var (_, bitmap) in document.LazyBitmaps)

+ 3 - 7
src/PixiEditor.AvaloniaUI/ViewModels/ViewModelMain.cs

@@ -96,6 +96,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     }
 
     public ActionDisplayList ActionDisplays { get; }
+    public bool UserWantsToClose { get; private set; }
 
     public ViewModelMain(IServiceProvider serviceProvider)
     {
@@ -168,14 +169,9 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     }
 
     [RelayCommand]
-    public async Task CloseWindow(object property)
+    public async Task CloseWindow()
     {
-        if (!(property is CancelEventArgs))
-        {
-            throw new ArgumentException();
-        }
-
-        ((CancelEventArgs)property).Cancel = !await DisposeAllDocumentsWithSaveConfirmation();
+        UserWantsToClose = await DisposeAllDocumentsWithSaveConfirmation();
     }
 
     private void ToolsSubViewModel_SelectedToolChanged(object sender, SelectedToolEventArgs e)

+ 0 - 14
src/PixiEditor.AvaloniaUI/Views/Decorators/AlwaysEnabled.cs

@@ -1,14 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-
-namespace PixiEditor.AvaloniaUI.Views.Decorators;
-
-public class AlwaysEnabled : Decorator
-{
-    static AlwaysEnabled()
-    {
-        IsEnabledProperty.OverrideMetadata(
-            typeof(AlwaysEnabled), /*TODO: Validate if coerce is needed*/
-            new StyledPropertyMetadata<bool>(true, coerce: (_, x) => x));
-    }
-}

+ 3 - 3
src/PixiEditor.AvaloniaUI/Views/Dialogs/ConfirmationPopup.axaml

@@ -18,20 +18,20 @@
                     Margin="0,0,10,15">
             <Button Margin="10,0,10,0" IsDefault="True" Padding="5 0"
                     ui:Translator.LocalizedString="{Binding FirstOptionText}"
-                    Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=popup}">
+                    Command="{Binding Path=SetConfirmationResultAndCloseCommand, ElementName=popup}">
                 <Button.CommandParameter>
                     <system:Boolean>True</system:Boolean>
                 </Button.CommandParameter>
             </Button>
             <Button Padding="5 0"
-                    Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=popup}"
+                    Command="{Binding Path=SetConfirmationResultAndCloseCommand, ElementName=popup}"
                     ui:Translator.LocalizedString="{Binding SecondOptionText}">
                 <Button.CommandParameter>
                     <system:Boolean>False</system:Boolean>
                 </Button.CommandParameter>
             </Button>
             <Button Margin="10,0,10,0" ui:Translator.Key="CANCEL"
-                    Command="{Binding DataContext.CancelCommand, ElementName=popup}" />
+                    Command="{Binding CancelCommand, ElementName=popup}" />
         </StackPanel>
 
         <TextBlock

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Dialogs/ConfirmationPopup.axaml.cs

@@ -54,7 +54,7 @@ internal partial class ConfirmationPopup : PixiEditorPopup
     }
 
     [RelayCommand]
-    private void SetResultAndClose(bool property)
+    public void SetConfirmationResultAndClose(bool property)
     {
         bool result = property;
         Result = result;

+ 27 - 0
src/PixiEditor.AvaloniaUI/Views/MainWindow.axaml.cs

@@ -1,12 +1,18 @@
+using System.Linq;
+using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.AvaloniaUI.Helpers;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.ExceptionHandling;
 using PixiEditor.AvaloniaUI.Models.IO;
+using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Platform;
@@ -104,4 +110,25 @@ internal partial class MainWindow : Window
             }
         }*/
     }
+
+    protected override void OnClosing(WindowClosingEventArgs e)
+    {
+        if (!DataContext.UserWantsToClose)
+        {
+            e.Cancel = true;
+            Task.Run(async () =>
+            {
+                await Dispatcher.UIThread.InvokeAsync(async () =>
+                {
+                    await DataContext.CloseWindowCommand.ExecuteAsync(null);
+                    if (DataContext.UserWantsToClose)
+                    {
+                        Close();
+                    }
+                });
+            });
+        }
+
+        base.OnClosing(e);
+    }
 }

+ 7 - 9
src/PixiEditor.AvaloniaUI/Views/Palettes/PaletteViewer.axaml

@@ -29,15 +29,13 @@
                                             HintColor="{Binding ElementName=paletteControl, Path=HintColor}"
                                             Colors="{Binding ElementName=paletteControl, Path=Colors}"/>
                     <StackPanel Margin="0, 0, 5, 0" HorizontalAlignment="Right" Width="110" VerticalAlignment="Center" Orientation="Horizontal">
-                        <decorators:AlwaysEnabled>
-                            <Button Margin="0, 0, 5, 0"
-                                    Classes="ToolButtonStyle" Click="BrowsePalettes_Click"
-                                    Cursor="Hand" Height="24" Width="24" ui:Translator.TooltipKey="BROWSE_PALETTES">
-                                <Button.Background>
-                                    <ImageBrush Source="/Images/Database.png"/>
-                                </Button.Background>
-                            </Button>
-                        </decorators:AlwaysEnabled>
+                        <Button Margin="0, 0, 5, 0"
+                                Classes="ToolButtonStyle" Click="BrowsePalettes_Click"
+                                Cursor="Hand" Height="24" Width="24" ui:Translator.TooltipKey="BROWSE_PALETTES">
+                            <Button.Background>
+                                <ImageBrush Source="/Images/Database.png"/>
+                            </Button.Background>
+                        </Button>
                         <Button Margin="0, 0, 5, 0" Classes="ToolButtonStyle"
                 Cursor="Hand" Height="24" Width="24" ui:Translator.TooltipKey="LOAD_PALETTE" Click="ImportPalette_OnClick">
                             <Button.Background>

+ 0 - 12
src/PixiEditor/App.xaml.cs

@@ -196,19 +196,7 @@ internal partial class App : Application
 
     protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
     {
-        base.OnSessionEnding(e);
 
-        var vm = ViewModelMain.Current;
-        if (vm is null)
-            return;
-
-        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
-        {
-            ConfirmationType confirmation = ConfirmationDialog.Show(
-                new LocalizedString("SESSION_UNSAVED_DATA", e.ReasonSessionEnding),
-                $"{e.ReasonSessionEnding}");
-            e.Cancel = confirmation != ConfirmationType.Yes;
-        }
     }
 
     private bool ParseArgument(string pattern, string args, out Group[] groups)