Browse Source

Some debug stuff fixed

Krzysztof Krysiński 2 years ago
parent
commit
4ef4297d0e

+ 34 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Behaviours/ClearFocusOnClickBehavior.cs

@@ -0,0 +1,34 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Xaml.Interactivity;
+using PixiEditor.Models.Controllers;
+
+namespace PixiEditor.Helpers.Behaviours;
+
+internal class ClearFocusOnClickBehavior : Behavior<Control>
+{
+    protected override void OnAttached()
+    {
+        base.OnAttached();
+        AssociatedObject.AddHandler(InputElement.PointerPressedEvent, AssociatedObject_MouseDown);
+        AssociatedObject.AddHandler(InputElement.LostFocusEvent, AssociatedObject_LostFocus);
+    }
+
+    private void AssociatedObject_LostFocus(object? sender, RoutedEventArgs? e)
+    {
+
+    }
+
+    protected override void OnDetaching()
+    {
+        AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_MouseDown);
+    }
+
+    private void AssociatedObject_MouseDown(object? sender, PointerPressedEventArgs? e)
+    {
+        AssociatedObject.Focus();
+        ShortcutController.UnblockShortcutExecutionAll();
+    }
+}

+ 33 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Extensions/ApplicationExtensions.cs

@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+
+namespace PixiEditor.Avalonia.Helpers.Extensions;
+
+public static class ApplicationExtensions
+{
+    public static void ForDesktopMainWindow(this Application app, Action<Window> action)
+    {
+        switch (app.ApplicationLifetime)
+        {
+            case IClassicDesktopStyleApplicationLifetime desktop:
+                action(desktop.MainWindow);
+                break;
+            default:
+                throw new NotSupportedException("ApplicationLifetime is not supported");
+        }
+    }
+
+    public static async Task ForDesktopMainWindowAsync(this Application app, Func<Window, Task> action)
+    {
+        switch (app.ApplicationLifetime)
+        {
+            case IClassicDesktopStyleApplicationLifetime desktop:
+                await action(desktop.MainWindow);
+                break;
+            default:
+                throw new NotSupportedException("ApplicationLifetime is not supported");
+        }
+    }
+}

+ 33 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/ConfirmationDialog.cs

@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Localization;
+using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Models.Dialogs;
+
+internal static class ConfirmationDialog
+{
+    public static async Task<ConfirmationType> Show(LocalizedString message, LocalizedString title)
+    {
+        ConfirmationPopup popup = new ConfirmationPopup
+        {
+            Title = title,
+            Body = message,
+            ShowInTaskbar = false
+        };
+
+        if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+        {
+            if (await popup.ShowDialog<bool>(desktop.MainWindow))
+            {
+                return popup.Result ? ConfirmationType.Yes : ConfirmationType.No;
+            }
+        }
+
+        return ConfirmationType.Canceled;
+    }
+}

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/PixiEditor.Avalonia.csproj

@@ -8,7 +8,7 @@
 
 
     <ItemGroup>
-        <AvaloniaResource Include="Assets\**" />
+      <AvaloniaResource Include="Assets\**" />
       <AvaloniaResource Include="Data\**" />
       <AvaloniaResource Include="Data\Languages\**" />
     </ItemGroup>

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/Document/DocumentManagerViewModel.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Windows.Input;
+using Avalonia.Input;
 using ChunkyImageLib.Operations;
 using PixiEditor.Avalonia.ViewModels;
 using PixiEditor.ChangeableDocument.Enums;

+ 11 - 11
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -43,11 +43,11 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     }
     
     [Command.Basic("PixiEditor.Clipboard.PasteReferenceLayer", "PASTE_REFERENCE_LAYER", "PASTE_REFERENCE_LAYER_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste")]
-    public void PasteReferenceLayer(DataObject data)
+    public async Task PasteReferenceLayer(DataObject data)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
-        var surface = (data == null ? ClipboardController.GetImagesFromClipboard() : ClipboardController.GetImage(data)).First();
+        var surface = (data == null ? await ClipboardController.GetImagesFromClipboard() : ClipboardController.GetImage(data)).First();
         using var image = surface.image;
         
         var bitmap = surface.image.ToWriteableBitmap();
@@ -79,9 +79,9 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Clipboard.PasteColor", false, "PASTE_COLOR", "PASTE_COLOR_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
     [Command.Basic("PixiEditor.Clipboard.PasteColorAsSecondary", true, "PASTE_COLOR_SECONDARY", "PASTE_COLOR_SECONDARY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
-    public void PasteColor(bool secondary)
+    public async Task PasteColor(bool secondary)
     {
-        if (!ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out var result))
+        if (!ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out var result))
         {
             return;
         }
@@ -110,7 +110,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsHex", CopyColor.SecondaryHEX, "COPY_COLOR_SECONDARY_HEX", "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
     [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsRgb", CopyColor.SecondardRGB, "COPY_COLOR_SECONDARY_RGB", "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
     [Command.Filter("PixiEditor.Clipboard.CopyColorToClipboard", "COPY_COLOR_TO_CLIPBOARD", "COPY_COLOR", Key = Key.C, Modifiers = KeyModifiers.Shift)]
-    public void CopyColorAsHex(CopyColor color)
+    public async Task CopyColorAsHex(CopyColor color)
     {
         var targetColor = color switch
         {
@@ -128,24 +128,24 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
                 : $"rgba({targetColor.R},{targetColor.G},{targetColor.B},{targetColor.A})",
         };
 
-        Clipboard.SetText(text);
+        await ClipboardController.Clipboard.SetTextAsync(text);
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPaste")]
-    public bool CanPaste(object parameter)
+    public async Task<bool> CanPaste(object parameter)
     {
-        return Owner.DocumentIsNotNull(null) && parameter is DataObject data ? ClipboardController.IsImage(data) : ClipboardController.IsImageInClipboard();
+        return Owner.DocumentIsNotNull(null) && parameter is DataObject data ? ClipboardController.IsImage(data) : await ClipboardController.IsImageInClipboard();
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteColor")]
-    public static bool CanPasteColor() => ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out _);
+    public static async Task<bool> CanPasteColor() => ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out _);
 
     [Evaluator.Icon("PixiEditor.Clipboard.PasteColorIcon")]
-    public static IImage GetPasteColorIcon()
+    public static async Task<IImage> GetPasteColorIcon()
     {
         Color color;
 
-        color = ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out var result) ? result.Value.ToOpaqueMediaColor() : Colors.Transparent;
+        color = ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out var result) ? result.Value.ToOpaqueMediaColor() : Colors.Transparent;
 
         return ColorSearchResult.GetIcon(color.ToOpaqueColor());
     }

+ 5 - 3
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/ColorsViewModel.cs

@@ -173,7 +173,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
             {
                 if (LocalPalettesFetcher.PaletteExists(palette.Name))
                 {
-                    var consent = ConfirmationDialog.Show(
+                    var consent = await ConfirmationDialog.Show(
                         new LocalizedString("OVERWRITE_PALETTE_CONSENT", palette.Name),
                         new LocalizedString("PALETTE_EXISTS"));
                     if (consent == ConfirmationType.No)
@@ -224,13 +224,15 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.Colors.ImportPalette", CanExecute = "PixiEditor.Colors.CanImportPalette")]
-    public void ImportPalette(List<PaletteColor> palette)
+    public async Task ImportPalette(List<PaletteColor> palette)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null || doc.Palette.SequenceEqual(palette))
             return;
 
-        if (doc.Palette.Count == 0 || ConfirmationDialog.Show(new LocalizedString("REPLACE_PALETTE_CONSENT"), new LocalizedString("REPLACE_PALETTE")) == ConfirmationType.Yes)
+        if (doc.Palette.Count == 0 || await ConfirmationDialog.Show(new LocalizedString(
+                    "REPLACE_PALETTE_CONSENT"),
+                new LocalizedString("REPLACE_PALETTE")) == ConfirmationType.Yes)
         {
             doc.Palette.ReplaceRange(palette.Select(x => new PaletteColor(x.R, x.G, x.B)));
         }

+ 97 - 61
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/DebugViewModel.cs

@@ -3,11 +3,14 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
 using Hardware.Info;
 using Newtonsoft.Json;
+using PixiEditor.Avalonia.Helpers.Extensions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Models.Commands.Attributes.Commands;
@@ -104,83 +107,103 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Debug("PixiEditor.Debug.DumpAllCommands", "DUMP_ALL_COMMANDS", "DUMP_ALL_COMMANDS_DESCRIPTIVE")]
-    public void DumpAllCommands()
+    public async Task DumpAllCommands()
     {
-        SaveFileDialog dialog = new SaveFileDialog();
-        var dialogResult = dialog.ShowDialog();
-        if (dialogResult.HasValue && dialogResult.Value)
+        await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
-            var commands = Owner.CommandController.Commands;
+            FilePickerSaveOptions options = new FilePickerSaveOptions();
+            options.DefaultExtension = "txt";
+            options.FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("Text") {Patterns = new [] {"*.txt"}} };
+            var pickedFile = desktop.StorageProvider.SaveFilePickerAsync(options).Result;
 
-            using StreamWriter writer = new StreamWriter(dialog.FileName);
-            foreach (var command in commands)
+            if (pickedFile != null)
             {
-                writer.WriteLine($"InternalName: {command.InternalName}");
-                writer.WriteLine($"Default Shortcut: {command.DefaultShortcut}");
-                writer.WriteLine($"IsDebug: {command.IsDebug}");
-                writer.WriteLine();
+                var commands = Owner.CommandController.Commands;
+
+                using StreamWriter writer = new StreamWriter(pickedFile.Path.AbsolutePath);
+                foreach (var command in commands)
+                {
+                    writer.WriteLine($"InternalName: {command.InternalName}");
+                    writer.WriteLine($"Default Shortcut: {command.DefaultShortcut}");
+                    writer.WriteLine($"IsDebug: {command.IsDebug}");
+                    writer.WriteLine();
+                }
             }
-        }
+        });
     }
     
     [Command.Debug("PixiEditor.Debug.GenerateKeysTemplate", "GENERATE_KEY_BINDINGS_TEMPLATE", "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE")]
-    public void GenerateKeysTemplate()
+    public async Task GenerateKeysTemplate()
     {
-        SaveFileDialog dialog = new SaveFileDialog();
-        var dialogResult = dialog.ShowDialog();
-        if (dialogResult.HasValue && dialogResult.Value)
+        await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
-            var commands = Owner.CommandController.Commands;
+            FilePickerSaveOptions options = new FilePickerSaveOptions();
+            options.DefaultExtension = "json";
+            options.FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("Json") {Patterns = new [] {"*.json"}} };
+            var pickedFile = await desktop.StorageProvider.SaveFilePickerAsync(options);
 
-            using StreamWriter writer = new StreamWriter(dialog.FileName);
-            Dictionary<string, KeyDefinition> keyDefinitions = new Dictionary<string, KeyDefinition>();
-            foreach (var command in commands)
+            if (pickedFile != null)
             {
-                if(command.IsDebug)
-                    continue;
-                keyDefinitions.Add($"(provider).{command.InternalName}", new KeyDefinition(command.InternalName, new HumanReadableKeyCombination("None"), Array.Empty<string>()));
-            }
+                var commands = Owner.CommandController.Commands;
 
-            writer.Write(JsonConvert.SerializeObject(keyDefinitions, Formatting.Indented));
-            writer.Close();
-            string file = File.ReadAllText(dialog.FileName);
-            foreach (var command in commands)
-            {
-                if(command.IsDebug)
-                    continue;
-                file = file.Replace($"(provider).{command.InternalName}", "");
+                using StreamWriter writer = new StreamWriter(pickedFile.Path.AbsolutePath);
+                Dictionary<string, KeyDefinition> keyDefinitions = new Dictionary<string, KeyDefinition>();
+                foreach (var command in commands)
+                {
+                    if(command.IsDebug)
+                        continue;
+                    keyDefinitions.Add($"(provider).{command.InternalName}", new KeyDefinition(command.InternalName, new HumanReadableKeyCombination("None"), Array.Empty<string>()));
+                }
+
+                writer.Write(JsonConvert.SerializeObject(keyDefinitions, Formatting.Indented));
+                writer.Close();
+                string file = await File.ReadAllTextAsync(pickedFile.Path.AbsolutePath);
+                foreach (var command in commands)
+                {
+                    if(command.IsDebug)
+                        continue;
+                    file = file.Replace($"(provider).{command.InternalName}", "");
+                }
+
+                await File.WriteAllTextAsync(pickedFile.Path.AbsolutePath, file);
+                IOperatingSystem.Current.OpenFolder(Path.GetDirectoryName(pickedFile.Path.AbsolutePath));
             }
-            
-            File.WriteAllText(dialog.FileName, file);
-            IOperatingSystem.Current.OpenFolder(dialog.FileName);
-        }
+        });
     }
 
     [Command.Debug("PixiEditor.Debug.ValidateShortcutMap", "VALIDATE_SHORTCUT_MAP", "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE")]
-    public void ValidateShortcutMap()
+    public async Task ValidateShortcutMap()
     {
-        OpenFileDialog dialog = new OpenFileDialog();
-        dialog.Filter = "Json files (*.json)|*.json";
-        dialog.InitialDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Data", "ShortcutActionMaps");
-        var dialogResult = dialog.ShowDialog();
-        
-        if (dialogResult.HasValue && dialogResult.Value)
+        await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
-            string file = File.ReadAllText(dialog.FileName);
-            var keyDefinitions = JsonConvert.DeserializeObject<Dictionary<string, KeyDefinition>>(file);
-            int emptyKeys = file.Split("\"\":").Length - 1;
-            int unknownCommands = 0;
-            
-            foreach (var keyDefinition in keyDefinitions)
+            FilePickerOpenOptions options = new FilePickerOpenOptions
+                {
+                    SuggestedStartLocation =
+                        await desktop.StorageProvider.TryGetFolderFromPathAsync(
+                            Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Data",
+                                "ShortcutActionMaps")),
+                    FileTypeFilter = new FilePickerFileType[] { new FilePickerFileType("Json") {Patterns = new [] {"*.json"}} }
+                };
+            var pickedFile = desktop.StorageProvider.OpenFilePickerAsync(options).Result.FirstOrDefault();
+
+            if (pickedFile != null)
             {
-                if (!Owner.CommandController.Commands.ContainsKey(keyDefinition.Value.Command))
+                string file = await File.ReadAllTextAsync(pickedFile.Path.AbsolutePath);
+                var keyDefinitions = JsonConvert.DeserializeObject<Dictionary<string, KeyDefinition>>(file);
+                int emptyKeys = file.Split("\"\":").Length - 1;
+                int unknownCommands = 0;
+
+                foreach (var keyDefinition in keyDefinitions)
                 {
-                    unknownCommands++;
+                    if (!Owner.CommandController.Commands.ContainsKey(keyDefinition.Value.Command))
+                    {
+                        unknownCommands++;
+                    }
                 }
-            }
 
-            NoticeDialog.Show(new LocalizedString("VALIDATION_KEYS_NOTICE_DIALOG", emptyKeys, unknownCommands), "RESULT");
-        }
+                NoticeDialog.Show(new LocalizedString("VALIDATION_KEYS_NOTICE_DIALOG", emptyKeys, unknownCommands), "RESULT");
+            }
+        });
     }
 
     [Command.Debug("PixiEditor.Debug.ClearRecentDocument", "CLEAR_RECENT_DOCUMENTS", "CLEAR_RECENTLY_OPENED_DOCUMENTS")]
@@ -212,15 +235,28 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.Debug.SetLanguageFromFilePicker")]
-    public void SetLanguageFromFilePicker()
+    public async Task SetLanguageFromFilePicker()
     {
-        var file = new OpenFileDialog { Filter = "key-value json (*.json)|*.json" };
-
-        if (file.ShowDialog().GetValueOrDefault())
+        await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
-            Owner.LocalizationProvider.LoadDebugKeys(
-                JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file.FileName)), false);
-        }
+            FilePickerOpenOptions options = new FilePickerOpenOptions
+            {
+                SuggestedStartLocation =
+                    await desktop.StorageProvider.TryGetFolderFromPathAsync(
+                        Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Data",
+                            "Languages")),
+                FileTypeFilter = new FilePickerFileType[] { new FilePickerFileType("key-value json") {Patterns = new [] {"*.json"}} }
+            };
+            var pickedFile = desktop.StorageProvider.OpenFilePickerAsync(options).Result.FirstOrDefault();
+
+            if (pickedFile != null)
+            {
+                Owner.LocalizationProvider.LoadDebugKeys(
+                    JsonConvert.DeserializeObject<Dictionary<string, string>>(
+                        await File.ReadAllTextAsync(pickedFile.Path.AbsolutePath)),
+                    false);
+            }
+        });
     }
 
     [Command.Debug("PixiEditor.Debug.OpenInstallDirectory", "OPEN_INSTALLATION_DIR", "OPEN_INSTALLATION_DIR", IconPath = "Folder.png")]

+ 2 - 2
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/FileViewModel.cs

@@ -144,9 +144,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.File.OpenFileFromClipboard", "OPEN_FILE_FROM_CLIPBOARD", "OPEN_FILE_FROM_CLIPBOARD_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.HasImageInClipboard")]
-    public void OpenFromClipboard()
+    public async Task OpenFromClipboard()
     {
-        var images = ClipboardController.GetImagesFromClipboard();
+        var images = await ClipboardController.GetImagesFromClipboard();
 
         foreach (var dataImage in images)
         {

+ 3 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/ViewModelMain.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using System.Linq;
 using System.Threading.Tasks;
+using System.Windows.Input;
 using CommunityToolkit.Mvvm.Input;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Avalonia.ViewModels;
@@ -11,6 +12,7 @@ using PixiEditor.Helpers.Collections;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
 using PixiEditor.ViewModels.SubViewModels.AdditionalContent;
@@ -236,7 +238,7 @@ internal partial class ViewModelMain : ViewModelBase
         ConfirmationType result = ConfirmationType.No;
         if (!document.AllChangesSaved)
         {
-            result = ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle);
+            result = await ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle);
             if (result == ConfirmationType.Yes)
             {
                 if (!await FileSubViewModel.SaveDocument(document, false))

+ 62 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/ConfirmationPopup.axaml

@@ -0,0 +1,62 @@
+<Window x:Class="PixiEditor.Views.Dialogs.ConfirmationPopup"
+        x:ClassModifier="internal"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns="https://github.com/avaloniaui"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+        xmlns:views="clr-namespace:PixiEditor.Views"
+        xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        xmlns:controls="https://github.com/avaloniaui"
+        xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime"
+        mc:Ignorable="d" d:Title="Unsaved changes"
+        Name="popup" WindowStartupLocation="CenterScreen" 
+        Height="180" Width="400" MinHeight="180" MinWidth="400"
+        ui:Translator.Key="{Binding ElementName=popup, Path=Title}"
+        FlowDirection="{helpers:Localization FlowDirection}">
+
+    <!--
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+    -->
+
+    <DockPanel Background="{DynamicResource ThemeBackgroundBrush1}" Focusable="True">
+        <Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </Interaction.Behaviors>
+
+        <dialogs:DialogTitleBar DockPanel.Dock="Top"
+            TitleKey="{Binding ElementName=popup, Path=Title}" CloseCommand="{Binding DataContext.CancelCommand, ElementName=popup}" />
+
+        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center"
+                    Margin="0,0,10,15">
+            <Button Margin="10,0,10,0" IsDefault="True" Padding="5 0"
+                    Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=popup}">
+                <Button.CommandParameter>
+                    <system:Boolean>True</system:Boolean>
+                </Button.CommandParameter>
+            </Button>
+            <Button Padding="5 0"
+                    Command="{Binding Path=DataContext.SetResultAndCloseCommand, 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}" />
+        </StackPanel>
+
+        <TextBlock
+                   Text="{Binding Body, ElementName=popup}" 
+                   HorizontalAlignment="Center" Margin="20,0" 
+                   TextWrapping="WrapWithOverflow" 
+                   TextTrimming="WordEllipsis"
+                   TextAlignment="Center"
+                   VerticalAlignment="Center" FontSize="15" Foreground="White" d:Text="The document has been modified. Do you want to save changes?"/>
+    </DockPanel>
+</Window>

+ 71 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/ConfirmationPopup.axaml.cs

@@ -0,0 +1,71 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.Views.Dialogs;
+
+/// <summary>
+///     Interaction logic for ConfirmationPopup.xaml
+/// </summary>
+internal partial class ConfirmationPopup : Window
+{
+    public static readonly StyledProperty<bool> ResultProperty =
+        AvaloniaProperty.Register<ConfirmationPopup, bool>(nameof(Result), true);
+
+    public static readonly StyledProperty<string> BodyProperty =
+        AvaloniaProperty.Register<ConfirmationPopup, string>(nameof(Body), string.Empty);
+
+    public static readonly StyledProperty<LocalizedString> FirstOptionTextProperty =
+        AvaloniaProperty.Register<ConfirmationPopup, LocalizedString>(nameof(FirstOptionText), new LocalizedString("YES"));
+
+    public static readonly StyledProperty<LocalizedString> SecondOptionTextProperty =
+        AvaloniaProperty.Register<ConfirmationPopup, LocalizedString>(nameof(SecondOptionText), new LocalizedString("NO"));
+
+    public LocalizedString FirstOptionText
+    {
+        get { return (LocalizedString)GetValue(FirstOptionTextProperty); }
+        set { SetValue(FirstOptionTextProperty, value); }
+    }
+
+    public LocalizedString SecondOptionText
+    {
+        get { return (LocalizedString)GetValue(SecondOptionTextProperty); }
+        set { SetValue(SecondOptionTextProperty, value); }
+    }
+    
+    public ConfirmationPopup()
+    {
+        InitializeComponent();
+        DataContext = this;
+    }
+
+
+    public bool Result
+    {
+        get => (bool)GetValue(ResultProperty);
+        set => SetValue(ResultProperty, value);
+    }
+
+
+    public string Body
+    {
+        get => (string)GetValue(BodyProperty);
+        set => SetValue(BodyProperty, value);
+    }
+
+    [RelayCommand]
+    private void SetResultAndClose(bool property)
+    {
+        bool result = property;
+        Result = result;
+        Close(true);
+    }
+
+    [RelayCommand]
+    private void Cancel()
+    {
+        Close(false);
+    }
+}

+ 31 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/DialogTitleBar.axaml

@@ -0,0 +1,31 @@
+<UserControl
+            x:Class="PixiEditor.Views.Dialogs.DialogTitleBar"
+            x:ClassModifier="internal"
+            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+            xmlns="https://github.com/avaloniaui"
+            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+            xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+            xmlns:views="clr-namespace:PixiEditor.Views"
+            xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+            xmlns:controls="https://github.com/avaloniaui"
+             mc:Ignorable="d"
+             x:Name="uc"
+             Height="35" d:DesignWidth="300">
+    <Grid Grid.Row="0" Background="{DynamicResource ThemeBackgroundBrush1}">
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition/>
+            <ColumnDefinition/>
+        </Grid.ColumnDefinitions>
+        <TextBlock 
+            TextAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center" 
+            ui:Translator.Key="{Binding ElementName=uc, Path=TitleKey}"
+            Foreground="White" 
+            FontSize="15" 
+            Margin="5,0,0,0" 
+            Grid.Column="0" Grid.ColumnSpan="2"/>
+        <Button Grid.Column="1" HorizontalAlignment="Right" IsCancel="True"
+                ui:Translator.TooltipKey="CLOSE"
+                    Command="{Binding ElementName=uc, Path=CloseCommand}" />
+    </Grid>
+</UserControl>

+ 44 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/DialogTitleBar.axaml.cs

@@ -0,0 +1,44 @@
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+
+namespace PixiEditor.Views.Dialogs;
+
+internal partial class DialogTitleBar : UserControl, ICustomTranslatorElement
+{
+    public static readonly StyledProperty<string> TitleKeyProperty =
+        AvaloniaProperty.Register<DialogTitleBar, string>(nameof(TitleKey), string.Empty);
+
+    public static readonly StyledProperty<ICommand> CloseCommandProperty =
+        AvaloniaProperty.Register<DialogTitleBar, ICommand>(nameof(CloseCommand));
+
+    public ICommand CloseCommand
+    {
+        get { return (ICommand)GetValue(CloseCommandProperty); }
+        set { SetValue(CloseCommandProperty, value); }
+    }
+
+    /// <summary>
+    /// The localization key of the window's title
+    /// </summary>
+    public string TitleKey
+    {
+        get { return (string)GetValue(TitleKeyProperty); }
+        set { SetValue(TitleKeyProperty, value); }
+    }
+
+    public DialogTitleBar()
+    {
+        InitializeComponent();
+    }
+
+    void ICustomTranslatorElement.SetTranslationBinding(AvaloniaProperty dependencyProperty, IObservable<string> binding)
+    {
+        Bind(dependencyProperty, binding);
+    }
+
+    AvaloniaProperty ICustomTranslatorElement.GetDependencyProperty()
+    {
+        return TitleKeyProperty;
+    }
+}

+ 1 - 1
src/PixiEditor.Extensions/UI/ICustomTranslatorElement.cs

@@ -5,6 +5,6 @@ namespace PixiEditor.Views;
 
 public interface ICustomTranslatorElement
 {
-    public void SetTranslationBinding(AvaloniaProperty dependencyProperty, IObservable<string> valueObservable);
+    public void SetTranslationBinding(AvaloniaProperty dependencyProperty, IObservable<string> binding);
     public AvaloniaProperty GetDependencyProperty();
 }

+ 8 - 10
src/PixiEditor/Views/Dialogs/ConfirmationPopup.xaml

@@ -1,43 +1,41 @@
 <Window x:Class="PixiEditor.Views.Dialogs.ConfirmationPopup"
         x:ClassModifier="internal"
-        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:helpers="clr-namespace:PixiEditor.Helpers"
         xmlns:system="clr-namespace:System;assembly=System.Runtime" 
         xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" 
         xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
-        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
         xmlns:views="clr-namespace:PixiEditor.Views"
-        xmlns:helpers="clr-namespace:PixiEditor.Helpers"
         xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
         mc:Ignorable="d" d:Title="Unsaved changes"
         Name="popup" WindowStartupLocation="CenterScreen" 
         Height="180" Width="400" MinHeight="180" MinWidth="400"
-        WindowStyle="None"
         ui:Translator.Key="{Binding ElementName=popup, Path=Title}"
         FlowDirection="{helpers:Localization FlowDirection}"
         d:DataContext="{d:DesignInstance dial:ConfirmationPopup}">
 
-    <WindowChrome.WindowChrome>
+    <!--<WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
-    </WindowChrome.WindowChrome>
+    </WindowChrome.WindowChrome>-->
 
-    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
+    <DockPanel Background="{DynamicResource ThemeBackgroundBrush1}" Focusable="True">
         <i:Interaction.Behaviors>
             <behaviours:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
 
-        <dial:DialogTitleBar DockPanel.Dock="Top"
+        <dialogs:DialogTitleBar DockPanel.Dock="Top"
             TitleKey="{Binding ElementName=popup, Path=Title}" CloseCommand="{Binding DataContext.CancelCommand, ElementName=popup}" />
 
         <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center"
                     Margin="0,0,10,15">
             <Button Margin="10,0,10,0" IsDefault="True" Padding="5 0"
                     Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=popup}"
-                    ui:Translator.LocalizedString="{Binding FirstOptionText}"
-                    Style="{StaticResource DarkRoundButton}">
+                    ui:Translator.LocalizedString="{Binding FirstOptionText}">
                 <Button.CommandParameter>
                     <system:Boolean>True</system:Boolean>
                 </Button.CommandParameter>