Equbuxu 1 год назад
Родитель
Сommit
db912e3c2b
35 измененных файлов с 1599 добавлено и 41 удалено
  1. 1 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/BoolOrToVisibilityConverter.cs
  2. 1 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/CountToVisibilityConverter.cs
  3. 1 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/EqualityBoolToIsVisibleConverter.cs
  4. 23 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/IsEqualConverter.cs
  5. 1 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/NotNullToVisibilityConverter.cs
  6. 24 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/SubtractConverter.cs
  7. 35 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/MarkupExtensions/EnumExtension.cs
  8. 7 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Dialogs/CustomDialog.cs
  9. 14 12
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Dialogs/OptionsDialog.cs
  10. 10 12
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Dialogs/ResizeDocumentDialog.cs
  11. 7 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj
  12. 2 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentManagerViewModel.cs
  13. 2 2
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/MainVM.cs
  14. 391 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/SettingsWindowViewModel.cs
  15. 10 5
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DebugViewModel.cs
  16. 2 3
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/WindowViewModel.cs
  17. 88 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/DiscordSettings.cs
  18. 57 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/FileSettings.cs
  19. 49 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/GeneralSettings.cs
  20. 16 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/ToolsSettings.cs
  21. 44 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/UpdateSettings.cs
  22. 30 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/SettingsGroup.cs
  23. 22 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/SettingsViewModel.cs
  24. 53 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Dialogs/OptionPopup.axaml
  25. 70 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Dialogs/OptionPopup.axaml.cs
  26. 6 6
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Dock/DocumentTemplate.axaml
  27. 11 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/MainWindow.axaml.cs
  28. 67 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ImportShortcutTemplatePopup.axaml
  29. 122 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ImportShortcutTemplatePopup.axaml.cs
  30. 46 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ShortcutsTemplateCard.axaml
  31. 64 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ShortcutsTemplateCard.axaml.cs
  32. 290 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Windows/SettingsWindow.axaml
  33. 29 0
      src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Windows/SettingsWindow.axaml.cs
  34. 2 0
      src/PixiEditor.sln.DotSettings
  35. 2 1
      src/PixiEditor/Views/Dialogs/SettingsWindow.xaml

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

@@ -8,6 +8,7 @@ using System.Windows;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
 
 namespace PixiEditor.Helpers.Converters;
+// TODO: seems like this converter is doing the same as the avalonia built in {x:Static BoolConverters.Or}
 internal class BoolOrToVisibilityConverter : SingleInstanceMultiValueConverter<BoolOrToVisibilityConverter>
 {
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/CountToVisibilityConverter.cs

@@ -2,6 +2,7 @@
 
 namespace PixiEditor.AvaloniaUI.Helpers.Converters;
 
+// TODO: the same should be doable by binding to the int directly and using a bang, e.g. {Binding !#someElem.Count}
 internal class CountToVisibilityConverter : SingleInstanceConverter<CountToVisibilityConverter>
 {
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/EqualityBoolToIsVisibleConverter.cs

@@ -2,6 +2,7 @@
 
 namespace PixiEditor.AvaloniaUI.Helpers.Converters;
 
+/// TODO: I refactored this logic in <see cref="IsEqualConverter"/>, all usages of this converter should be replaced with IsEqualConverter
 internal class EqualityBoolToIsVisibleConverter : MarkupConverter
 {
     public bool Invert { get; set; }

+ 23 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/IsEqualConverter.cs

@@ -0,0 +1,23 @@
+using System.Globalization;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class IsEqualConverter : SingleInstanceConverter<IsEqualConverter>
+{
+    public override object Convert(object? value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is null)
+            return true;
+
+        if (value.GetType().IsAssignableTo(typeof(Enum)) && parameter is string s)
+        {
+            parameter = Enum.Parse(value.GetType(), s);
+        }
+        else
+        {
+            parameter = System.Convert.ChangeType(parameter, value.GetType());
+        }
+
+        return value.Equals(parameter);
+    }
+}

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/NotNullToVisibilityConverter.cs

@@ -3,6 +3,7 @@ using PixiEditor.AvaloniaUI.Helpers.Converters;
 
 namespace PixiEditor.Helpers.Converters;
 
+// TODO: check if this converter can be replaced with StringConverters.IsNullOrEmpty, StringConverters.IsNotNullOrEmpty, ObjectConverters.IsNull, or ObjectConverters.IsNotNull
 internal class NotNullToVisibilityConverter
     : MarkupConverter
 {

+ 24 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/Converters/SubtractConverter.cs

@@ -0,0 +1,24 @@
+using System.Globalization;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class SubtractConverter : SingleInstanceConverter<SubtractConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        object parsedValue = value is string stringValue ? double.Parse(stringValue) : value;
+        object parsedParameter = parameter is string parameterString ? double.Parse(parameterString) : parameter;
+        
+        if (parsedValue is not double doubleValue)
+        {
+            return value;
+        }
+
+        if (parsedParameter is not double doubleParameter)
+        {
+            return value;
+        }
+
+        return doubleValue - doubleParameter;
+    }
+}

+ 35 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Helpers/MarkupExtensions/EnumExtension.cs

@@ -0,0 +1,35 @@
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Helpers.MarkupExtensions;
+
+internal class EnumExtension : MarkupExtension
+{
+    private Type enumType;
+
+    public EnumExtension(Type enumType)
+    {
+        EnumType = enumType ?? throw new ArgumentNullException(nameof(enumType));
+    }
+
+    private Type EnumType
+    {
+        get => enumType;
+        init
+        {
+            if (this.enumType == value)
+                return;
+
+            var type = Nullable.GetUnderlyingType(value) ?? value;
+
+            if (type.IsEnum == false)
+                throw new ArgumentException("Type must be an Enum.");
+
+            this.enumType = value;
+        }
+    }
+
+    public override object ProvideValue(IServiceProvider serviceProvider) // or IXamlServiceProvider for UWP and WinUI
+    {
+        return Enum.GetValues(EnumType);
+    }
+}

+ 7 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Dialogs/CustomDialog.cs

@@ -1,9 +1,16 @@
 using System.Threading.Tasks;
+using Avalonia.Controls;
 using CommunityToolkit.Mvvm.ComponentModel;
 
 namespace PixiEditor.AvaloniaUI.Models.Dialogs;
 
 internal abstract class CustomDialog : ObservableObject
 {
+    protected Window OwnerWindow { get; }
+
+    public CustomDialog(Window ownerWindow)
+    {
+        this.OwnerWindow = ownerWindow;
+    }
     public abstract Task<bool> ShowDialog();
 }

+ 14 - 12
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Dialogs/OptionsDialog.cs

@@ -1,25 +1,28 @@
 using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Layout;
 using Avalonia.Media;
+using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.Extensions.Common.Localization;
 
 namespace PixiEditor.AvaloniaUI.Models.Dialogs;
 
-internal class OptionsDialog<T> : CustomDialog, IEnumerable<T>
+internal class OptionsDialog<T> : CustomDialog, IEnumerable<T> 
+    where T : notnull
 {
-    private Dictionary<T, Action<T>> _results = new();
+    private Dictionary<T, Action<T>?> _results = new();
 
     public string Title { get; set; }
 
     public object Content { get; set; }
 
-    public T Result { get; private set; }
+    public T? Result { get; private set; }
 
-    public OptionsDialog(LocalizedString title, object content)
+    public OptionsDialog(LocalizedString title, object content, Window ownerWindow) : base(ownerWindow)
     {
         Title = title;
 
@@ -42,12 +45,13 @@ internal class OptionsDialog<T> : CustomDialog, IEnumerable<T>
         }
     }
 
-    public OptionsDialog(string title, object content, IEnumerable<KeyValuePair<T, Action<T>>> options) : this(title, content)
+    public OptionsDialog(string title, object content, IEnumerable<KeyValuePair<T, Action<T>>> options, Window ownerWindow) 
+        : this(title, content, ownerWindow)
     {
         _results = new(options);
     }
 
-    public Action<T> this[T name]
+    public Action<T>? this[T name]
     {
         get => _results[name];
         set => _results.Add(name, value);
@@ -55,18 +59,16 @@ internal class OptionsDialog<T> : CustomDialog, IEnumerable<T>
 
     public override async Task<bool> ShowDialog()
     {
-        //TODO: Implement
-        /*var popup = new OptionPopup(Title, Content, new(_results.Keys.Select(x => (object)x)));
-        var popupResult = popup.ShowDialog();
+        var popup = new OptionPopup(Title, Content, new(_results.Keys.Select(x => (object)x)));
+        await popup.ShowDialog(OwnerWindow);
 
-        Result = (T)popup.Result;
+        Result = (T?)popup.Result;
         if (Result != null)
         {
             _results[Result]?.Invoke(Result);
         }
 
-        return popupResult.GetValueOrDefault(false);*/
-        return false;
+        return Result is not null;
     }
 
     public void Add(T name) => _results.Add(name, null);

+ 10 - 12
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Models/Dialogs/ResizeDocumentDialog.cs

@@ -1,5 +1,6 @@
 using System.Threading.Tasks;
 using Avalonia;
+using Avalonia.Controls;
 using PixiEditor.AvaloniaUI.Helpers.Extensions;
 using PixiEditor.AvaloniaUI.Views.Windows;
 using PixiEditor.ChangeableDocument.Enums;
@@ -11,7 +12,7 @@ internal class ResizeDocumentDialog : CustomDialog
     private int height;
     private int width;
 
-    public ResizeDocumentDialog(int currentWidth, int currentHeight, bool openResizeCanvas = false)
+    public ResizeDocumentDialog(int currentWidth, int currentHeight, Window owner, bool openResizeCanvas = false) : base(owner)
     {
         Width = currentWidth;
         Height = currentHeight;
@@ -64,19 +65,16 @@ internal class ResizeDocumentDialog : CustomDialog
             NewSelectedUnit = SizeUnit.Pixel
         };
 
-        await Application.Current.ForDesktopMainWindowAsync(async window =>
+        var result = await popup.ShowDialog<bool>(OwnerWindow);
+        if (result)
         {
-            var result = await popup.ShowDialog<bool>(window);
-            if (result)
+            Width = popup.NewAbsoluteWidth;
+            Height = popup.NewAbsoluteHeight;
+            /*if (popup is ResizeCanvasPopup resizeCanvas) TODO: Implement
             {
-                Width = popup.NewAbsoluteWidth;
-                Height = popup.NewAbsoluteHeight;
-                /*if (popup is ResizeCanvasPopup resizeCanvas) TODO: Implement
-                {
-                    ResizeAnchor = resizeCanvas.SelectedAnchorPoint;
-                }*/
-            }
-        });
+                ResizeAnchor = resizeCanvas.SelectedAnchorPoint;
+            }*/
+        }
 
         return false;
     }

+ 7 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj

@@ -74,4 +74,11 @@
       <UpToDateCheckInput Remove="Fonts\feather.ttf" />
     </ItemGroup>
   
+    <ItemGroup>
+      <Compile Update="Views\Shortcuts\ImportShortcutTemplatePopup.axaml.cs">
+        <DependentUpon>ImportShortcutTemplatePopup.axaml</DependentUpon>
+        <SubType>Code</SubType>
+      </Compile>
+    </ItemGroup>
+  
 </Project>

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

@@ -8,6 +8,7 @@ using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
+using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -155,6 +156,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ResizeDocumentDialog dialog = new ResizeDocumentDialog(
             doc.Width,
             doc.Height,
+            MainWindow.Current!,
             canvas);
         if (await dialog.ShowDialog())
         {

+ 2 - 2
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/MainVM.cs

@@ -1,8 +1,8 @@
 using System.Collections.Generic;
 using Avalonia.Markup.Xaml;
-using PixiEditor.AvaloniaUI.ViewModels;
+using PixiEditor.ViewModels;
 
-namespace PixiEditor.ViewModels;
+namespace PixiEditor.AvaloniaUI.ViewModels;
 internal class MainVM : MarkupExtension
 {
     private MainVmEnum? vm;

+ 391 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/SettingsWindowViewModel.cs

@@ -0,0 +1,391 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Platform.Storage;
+using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Exceptions;
+using PixiEditor.AvaloniaUI.Models.Commands;
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using PixiEditor.AvaloniaUI.Models.Commands.Templates;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
+using PixiEditor.AvaloniaUI.ViewModels.UserPreferences;
+using PixiEditor.AvaloniaUI.Views;
+using PixiEditor.AvaloniaUI.Views.Shortcuts;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.ViewModels;
+
+internal class SettingsPage : ObservableObject
+{
+    private LocalizedString name;
+
+    public LocalizedString Name
+    {
+        get => name;
+        set => SetProperty(ref name, value);
+    }
+
+    public SettingsPage(string nameKey)
+    {
+        Name = new LocalizedString(nameKey);
+    }
+
+    public void UpdateName()
+    {
+        Name = new LocalizedString(Name.Key);
+    }
+}
+
+internal class SettingsWindowViewModel : ViewModelBase
+{
+    private string searchTerm;
+    private int visibleGroups;
+    private int currentPage;
+
+    public bool ShowUpdateTab
+    {
+        get
+        {
+#if UPDATE || DEBUG
+            return true;
+#else
+                return false;
+#endif
+        }
+    }
+
+    public string SearchTerm
+    {
+        get => searchTerm;
+        set
+        {
+            string oldValue = searchTerm;
+            if (SetProperty(ref searchTerm, value) &&
+                !(string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(oldValue)))
+            {
+                UpdateSearchResults();
+                VisibleGroups = Commands.Count(x => x.IsVisible);
+            }
+        }
+    }
+
+    public int CurrentPage
+    {
+        get => currentPage;
+        set => SetProperty(ref currentPage, value);
+    }
+
+    public int VisibleGroups
+    {
+        get => visibleGroups;
+        private set => SetProperty(ref visibleGroups, value);
+    }
+
+    public SettingsViewModel SettingsSubViewModel { get; set; }
+
+    public List<GroupSearchResult> Commands { get; }
+    public ObservableCollection<SettingsPage> Pages { get; }
+
+    private static List<ICustomShortcutFormat>? customShortcutFormats;
+
+    [Command.Internal("PixiEditor.Shortcuts.Reset")]
+    public static async Task ResetCommand()
+    {
+        await new OptionsDialog<string>("ARE_YOU_SURE", new LocalizedString("WARNING_RESET_SHORTCUTS_DEFAULT"), MainWindow.Current!)
+        {
+            { new LocalizedString("YES"), x => CommandController.Current.ResetShortcuts() },
+            new LocalizedString("CANCEL"),
+        }.ShowDialog();
+    }
+
+    [Command.Internal("PixiEditor.Shortcuts.Export")]
+    public static async Task ExportShortcuts()
+    {
+        var file = await MainWindow.Current!.StorageProvider.SaveFilePickerAsync(new()
+        {
+            SuggestedStartLocation = await MainWindow.Current!.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents),
+            FileTypeChoices = new List<FilePickerFileType>()
+            {
+                new FilePickerFileType("PixiShorts (*.pixisc)")
+                {
+                    Patterns = new List<string>
+                    {
+                        "*.pixisc"
+                    },
+                },
+                new FilePickerFileType("json (*.json)")
+                {
+                    Patterns = new List<string>
+                    {
+                        "*.json"
+                    },
+                },
+                new FilePickerFileType("All files (*.*)")
+                {
+                    Patterns = new List<string>
+                    {
+                        "*.*"
+                    },
+                },
+            },
+        });
+        
+        
+        if (file is not null)
+        {
+            File.Copy(CommandController.ShortcutsPath, file.Path.AbsolutePath, true);
+        }
+        
+        // Sometimes, focus was brought back to the last edited shortcut
+        // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
+    }
+
+    [Command.Internal("PixiEditor.Shortcuts.Import")]
+    public static async Task ImportShortcuts()
+    {
+        List<FilePickerFileType> fileTypes = new List<FilePickerFileType>
+        {
+            new("PixiShorts (*.pixisc)")
+            {
+                Patterns = new List<string>
+                {
+                    "*.pixisc"
+                },
+            },
+            new("json (*.json)")
+            {
+                Patterns = new List<string>
+                {
+                    "*.json"
+                },
+            },
+        };
+        
+        customShortcutFormats ??= ShortcutProvider.GetProviders().OfType<ICustomShortcutFormat>().ToList();
+        AddCustomParsersFormat(customShortcutFormats, fileTypes);
+        
+        fileTypes.Add(new FilePickerFileType("All files (*.*)")
+        {
+            Patterns = new List<string>
+            {
+                "*.*"
+            },
+        });
+        
+        fileTypes.Insert(0, new FilePickerFileType($"All Shortcut files {string.Join(",", fileTypes.SelectMany(a => a.Patterns))}")
+        {
+            Patterns = fileTypes.SelectMany(a => a.Patterns).ToList(),
+        });
+        
+        IReadOnlyList<IStorageFile> files = await MainWindow.Current!.StorageProvider.OpenFilePickerAsync(new()
+        {
+            AllowMultiple = false,
+            SuggestedStartLocation = await MainWindow.Current!.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents),
+            FileTypeFilter = fileTypes,
+        });
+        
+        if (files.Count > 0)
+        {
+            List<Shortcut> shortcuts = new List<Shortcut>();
+            if (!TryImport(files[0], ref shortcuts))
+                return;
+            
+            CommandController.Current.ResetShortcuts();
+            CommandController.Current.Import(shortcuts, false);
+            File.Copy(files[0].Path.AbsolutePath, CommandController.ShortcutsPath, true);
+            NoticeDialog.Show("SHORTCUTS_IMPORTED_SUCCESS", "SUCCESS");
+        }
+        
+        // Sometimes, focus was brought back to the last edited shortcut
+        // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
+    }
+
+    private static bool TryImport(IStorageFile file, ref List<Shortcut> shortcuts)
+    {
+        if (file.Name.EndsWith(".pixisc") || file.Name.EndsWith(".json"))
+        {
+            try
+            {
+                shortcuts = ShortcutFile.LoadTemplate(file.Path.AbsolutePath)?.Shortcuts.ToList();
+            }
+            catch (Exception)
+            {
+                NoticeDialog.Show(title: "ERROR", message: "ERROR_READING_FILE");
+                return false;
+            }
+
+            if (shortcuts is null)
+            {
+                NoticeDialog.Show("SHORTCUTS_FILE_INCORRECT_FORMAT", "INVALID_FILE");
+                return false;
+            }
+        }
+        else
+        {
+            var provider = customShortcutFormats.FirstOrDefault(x =>
+                x.CustomShortcutExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase));
+            if (provider is null)
+            {
+                NoticeDialog.Show("UNSUPPORTED_FILE_FORMAT", "INVALID_FILE");
+                return false;
+            }
+
+            try
+            {
+                shortcuts = provider.KeysParser.Parse(file.Path.AbsolutePath, false)?.Shortcuts.ToList();
+            }
+            catch (RecoverableException e)
+            {
+                NoticeDialog.Show(title: "ERROR", message: e.DisplayMessage);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static void AddCustomParsersFormat(IList<ICustomShortcutFormat>? customFormats, List<FilePickerFileType> listToAddTo)
+    {
+        if (customFormats is null || customFormats.Count == 0)
+            return;
+
+        foreach (ICustomShortcutFormat format in customFormats)
+        {
+            foreach (var extension in format.CustomShortcutExtensions)
+            {
+                string extensionWithoutDot = extension.TrimStart('.');
+                listToAddTo.Add(new FilePickerFileType(extensionWithoutDot)
+                {
+                    Patterns = new[] { $"*.{extensionWithoutDot}" },
+                });
+            }
+        }
+    }
+
+    [Command.Internal("PixiEditor.Shortcuts.OpenTemplatePopup")]
+    public static void OpenTemplatePopup()
+    {
+        ImportShortcutTemplatePopup popup = new ImportShortcutTemplatePopup();
+        popup.ShowDialog(MainWindow.Current!);
+    }
+
+    public SettingsWindowViewModel()
+    {
+        Pages = new ObservableCollection<SettingsPage>
+        {
+            new SettingsPage("GENERAL"),
+            new SettingsPage("DISCORD"),
+            new SettingsPage("KEY_BINDINGS"),
+        };
+
+        ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
+        Commands = new(CommandController.Current.CommandGroups.Select(x => new GroupSearchResult(x)));
+        UpdateSearchResults();
+        SettingsSubViewModel = new SettingsViewModel(this);
+        ViewModelMain.Current.Preferences.AddCallback("IsDebugModeEnabled", _ => UpdateSearchResults());
+        VisibleGroups = Commands.Count(x => x.IsVisible);
+    }
+
+    private void UpdatePages()
+    {
+        foreach (var page in Pages)
+        {
+            page.UpdateName();
+        }
+
+        OnPropertyChanged(nameof(Pages));
+    }
+
+    private void OnLanguageChanged(Language obj)
+    {
+        UpdatePages();
+    }
+
+    private void UpdateSearchResults()
+    {
+        if (string.IsNullOrWhiteSpace(searchTerm))
+        {
+            foreach (var group in Commands)
+            {
+                var visibleCommands = 0;
+
+                foreach (var command in group.Commands)
+                {
+                    if ((command.Command.IsDebug && ViewModelMain.Current.DebugSubViewModel.UseDebug) ||
+                        !command.Command.IsDebug)
+                    {
+                        visibleCommands++;
+                        command.IsVisible = true;
+                    }
+                    else
+                    {
+                        command.IsVisible = false;
+                    }
+                }
+
+                group.IsVisible = visibleCommands > 0;
+            }
+            return;
+        }
+
+        foreach (var group in Commands)
+        {
+            var visibleCommands = 0;
+            foreach (var command in group.Commands)
+            {
+                if (command.Command.DisplayName.Value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase))
+                {
+                    visibleCommands++;
+                    command.IsVisible = true;
+                }
+                else
+                {
+                    command.IsVisible = false;
+                }
+            }
+
+            group.IsVisible = visibleCommands > 0;
+        }
+    }
+
+    internal class GroupSearchResult : ObservableObject
+    {
+        private bool isVisible;
+
+        public LocalizedString DisplayName { get; set; }
+
+        public List<CommandSearchResult> Commands { get; set; }
+
+        public bool IsVisible
+        {
+            get => isVisible;
+            set => SetProperty(ref isVisible, value);
+        }
+
+        public GroupSearchResult(CommandGroup group)
+        {
+            DisplayName = group.DisplayName;
+            Commands = new(group.VisibleCommands.Select(x => new CommandSearchResult(x)));
+        }
+    }
+
+    internal class CommandSearchResult : ObservableObject
+    {
+        private bool isVisible;
+
+        public Models.Commands.Commands.Command Command { get; set; }
+
+        public bool IsVisible
+        {
+            get => isVisible;
+            set => SetProperty(ref isVisible, value);
+        }
+
+        public CommandSearchResult(Models.Commands.Commands.Command command)
+        {
+            Command = command;
+        }
+    }
+}

+ 10 - 5
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DebugViewModel.cs

@@ -12,6 +12,7 @@ using PixiEditor.AvaloniaUI.Helpers.Extensions;
 using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.Commands.Templates.Providers.Parsers;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
+using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.OperatingSystem;
@@ -270,8 +271,11 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     [Command.Debug("PixiEditor.Debug.DeleteUserPreferences", @"%appdata%\PixiEditor\user_preferences.json", "DELETE_USR_PREFS", "DELETE_USR_PREFS")]
     [Command.Debug("PixiEditor.Debug.DeleteShortcutFile", @"%appdata%\PixiEditor\shortcuts.json", "DELETE_SHORTCUT_FILE", "DELETE_SHORTCUT_FILE")]
     [Command.Debug("PixiEditor.Debug.DeleteEditorData", @"%localappdata%\PixiEditor\editor_data.json", "DELETE_EDITOR_DATA", "DELETE_EDITOR_DATA")]
-    public static void DeleteFile(string path)
+    public static async Task DeleteFile(string path)
     {
+        if (MainWindow.Current is null)
+            return;
+        
         string file = Environment.ExpandEnvironmentVariables(path);
         if (!File.Exists(file))
         {
@@ -279,13 +283,14 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
             return;
         }
 
-        OptionsDialog<string> dialog = new("ARE_YOU_SURE", new LocalizedString("ARE_YOU_SURE_PATH_FULL_PATH", path, file))
+        OptionsDialog<string> dialog = new("ARE_YOU_SURE", new LocalizedString("ARE_YOU_SURE_PATH_FULL_PATH", path, file), MainWindow.Current)
         {
-            { "Yes", x => File.Delete(file) },
-            "Cancel"
+            // TODO: seems like this should be localized strings
+            { new LocalizedString("YES"), x => File.Delete(file) },
+            new LocalizedString("CANCEL")
         };
 
-        dialog.ShowDialog();
+        await dialog.ShowDialog();
     }
 
     [Conditional("DEBUG")]

+ 2 - 3
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/WindowViewModel.cs

@@ -138,9 +138,8 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
             page = 0;
         }
 
-        //TODO: Add settings window
-        /*var settings = new SettingsWindow(page);
-        settings.Show();*/
+        var settings = new SettingsWindow(page);
+        settings.Show();
     }
 
     [Command.Basic("PixiEditor.Window.OpenStartupWindow", "OPEN_STARTUP_WINDOW", "OPEN_STARTUP_WINDOW")]

+ 88 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/DiscordSettings.cs

@@ -0,0 +1,88 @@
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences.Settings;
+
+internal class DiscordSettings : SettingsGroup
+{
+    private bool enableRichPresence = GetPreference(nameof(EnableRichPresence), true);
+
+    public bool EnableRichPresence
+    {
+        get => enableRichPresence;
+        set
+        {
+            enableRichPresence = value;
+            RaiseAndUpdatePreference(nameof(EnableRichPresence), value);
+        }
+    }
+
+    private bool showDocumentName = GetPreference(nameof(ShowDocumentName), false);
+
+    public bool ShowDocumentName
+    {
+        get => showDocumentName;
+        set
+        {
+            showDocumentName = value;
+            RaiseAndUpdatePreference(nameof(ShowDocumentName), value);
+            OnPropertyChanged(nameof(DetailPreview));
+        }
+    }
+
+    private bool showDocumentSize = GetPreference(nameof(ShowDocumentSize), true);
+
+    public bool ShowDocumentSize
+    {
+        get => showDocumentSize;
+        set
+        {
+            showDocumentSize = value;
+            RaiseAndUpdatePreference(nameof(ShowDocumentSize), value);
+            OnPropertyChanged(nameof(StatePreview));
+        }
+    }
+
+    private bool showLayerCount = GetPreference(nameof(ShowLayerCount), true);
+
+    public bool ShowLayerCount
+    {
+        get => showLayerCount;
+        set
+        {
+            showLayerCount = value;
+            RaiseAndUpdatePreference(nameof(ShowLayerCount), value);
+            OnPropertyChanged(nameof(StatePreview));
+        }
+    }
+
+    public string DetailPreview
+    {
+        get
+        {
+            return ShowDocumentName ? $"Editing coolPixelArt.pixi" : "Editing an image";
+        }
+    }
+
+    public string StatePreview
+    {
+        get
+        {
+            string state = string.Empty;
+
+            if (ShowDocumentSize)
+            {
+                state = "16x16";
+            }
+
+            if (ShowDocumentSize && ShowLayerCount)
+            {
+                state += ", ";
+            }
+
+            if (ShowLayerCount)
+            {
+                state += $"2 Layers";
+            }
+
+            return state;
+        }
+    }
+}

+ 57 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/FileSettings.cs

@@ -0,0 +1,57 @@
+using PixiEditor.AvaloniaUI.Models;
+using PixiEditor.Extensions.Common.UserPreferences;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences.Settings;
+
+internal class FileSettings : SettingsGroup
+{
+    private bool showStartupWindow = GetPreference(nameof(ShowStartupWindow), true);
+
+    public bool ShowStartupWindow
+    {
+        get => showStartupWindow;
+        set => RaiseAndUpdatePreference(ref showStartupWindow, value);
+    }
+
+    private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
+
+    public int DefaultNewFileWidth
+    {
+        get => defaultNewFileWidth;
+        set
+        {
+            defaultNewFileWidth = value;
+            string name = nameof(DefaultNewFileWidth);
+            RaiseAndUpdatePreference(name, value);
+        }
+    }
+
+    private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
+
+    public int DefaultNewFileHeight
+    {
+        get => defaultNewFileHeight;
+        set
+        {
+            defaultNewFileHeight = value;
+            string name = nameof(DefaultNewFileHeight);
+            RaiseAndUpdatePreference(name, value);
+        }
+    }
+
+    private int maxOpenedRecently = GetPreference(PreferencesConstants.MaxOpenedRecently, PreferencesConstants.MaxOpenedRecentlyDefault);
+
+    public int MaxOpenedRecently
+    {
+        get => maxOpenedRecently;
+        set => RaiseAndUpdatePreference(ref maxOpenedRecently, value);
+    }
+
+    private bool disableNewsPanel = GetPreference(PreferencesConstants.DisableNewsPanel, false);
+
+    public bool DisableNewsPanel
+    {
+        get => disableNewsPanel;
+        set => RaiseAndUpdatePreference(ref disableNewsPanel, value, PreferencesConstants.DisableNewsPanel);
+    }
+}

+ 49 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/GeneralSettings.cs

@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences.Settings;
+
+internal class GeneralSettings : SettingsGroup
+{
+    private bool imagePreviewInTaskbar = GetPreference(nameof(ImagePreviewInTaskbar), false);
+    private LanguageData? selectedLanguage = ILocalizationProvider.Current?.SelectedLanguage;
+    private List<LanguageData>? availableLanguages = ILocalizationProvider.Current?.LocalizationData.Languages
+        .OrderByDescending(x => x == ILocalizationProvider.Current.FollowSystem)
+        .ThenByDescending(x => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code || CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code)
+        .ThenBy(x => x.Name).ToList();
+
+    public bool ImagePreviewInTaskbar
+    {
+        get => imagePreviewInTaskbar;
+        set => RaiseAndUpdatePreference(ref imagePreviewInTaskbar, value);
+    }
+
+    private bool isDebugModeEnabled = GetPreference(nameof(IsDebugModeEnabled), false);
+    public bool IsDebugModeEnabled
+    {
+        get => isDebugModeEnabled;
+        set => RaiseAndUpdatePreference(ref isDebugModeEnabled, value);
+    }
+    
+    public List<LanguageData>? AvailableLanguages
+    {
+        get => availableLanguages;
+        set => SetProperty(ref availableLanguages, value);
+    }
+
+    public LanguageData? SelectedLanguage
+    {
+        get => selectedLanguage;
+        set
+        {
+            if (SetProperty(ref selectedLanguage, value))
+            {
+                ILocalizationProvider.Current?.LoadLanguage(value);
+                IPreferences.Current.UpdatePreference("LanguageCode", value.Code);
+            }
+        }
+    }
+}

+ 16 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/ToolsSettings.cs

@@ -0,0 +1,16 @@
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences.Settings;
+
+internal class ToolsSettings : SettingsGroup
+{
+    private bool enableSharedToolbar = GetPreference(nameof(EnableSharedToolbar), false);
+
+    public bool EnableSharedToolbar
+    {
+        get => enableSharedToolbar;
+        set
+        {
+            enableSharedToolbar = value;
+            RaiseAndUpdatePreference(nameof(EnableSharedToolbar), value);
+        }
+    }
+}

+ 44 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/Settings/UpdateSettings.cs

@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences.Settings;
+
+internal class UpdateSettings : SettingsGroup
+{
+    private bool checkUpdatesOnStartup = GetPreference("CheckUpdatesOnStartup", true);
+
+    public bool CheckUpdatesOnStartup
+    {
+        get => checkUpdatesOnStartup;
+        set
+        {
+            checkUpdatesOnStartup = value;
+            string name = nameof(CheckUpdatesOnStartup);
+            RaiseAndUpdatePreference(name, value);
+        }
+    }
+
+    private string updateChannelName =
+#if UPDATE
+        GetPreference("UpdateChannel", "Release");
+#else
+        ViewModelMain.Current?.UpdateSubViewModel?.UpdateChannels?.FirstOrDefault()?.Name ?? "Unknown";
+#endif
+
+    public string UpdateChannelName
+    {
+        get => updateChannelName;
+        set
+        {
+            updateChannelName = value;
+#if UPDATE
+            RaiseAndUpdatePreference("UpdateChannel", value);
+#endif
+        }
+    }
+
+    public IEnumerable<string> UpdateChannels
+    {
+        get => ViewModelMain.Current.UpdateSubViewModel.UpdateChannels.Select(x => x.Name);
+    }
+}

+ 30 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/SettingsGroup.cs

@@ -0,0 +1,30 @@
+using System.Runtime.CompilerServices;
+using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.Extensions.Common.UserPreferences;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences;
+
+internal class SettingsGroup : ObservableObject
+{
+    protected static T GetPreference<T>(string name)
+    {
+        return IPreferences.Current.GetPreference<T>(name);
+    }
+
+    protected static T? GetPreference<T>(string name, T? fallbackValue)
+    {
+        return IPreferences.Current.GetPreference(name, fallbackValue);
+    }
+
+    protected void RaiseAndUpdatePreference<T>(string name, T value)
+    {
+        OnPropertyChanged(name);
+        IPreferences.Current.UpdatePreference(name, value);
+    }
+
+    protected void RaiseAndUpdatePreference<T>(ref T backingStore, T value, [CallerMemberName] string name = "")
+    {
+        SetProperty(ref backingStore, value, propertyName: name);
+        IPreferences.Current.UpdatePreference(name, value);
+    }
+}

+ 22 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/ViewModels/UserPreferences/SettingsViewModel.cs

@@ -0,0 +1,22 @@
+using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
+using PixiEditor.AvaloniaUI.ViewModels.UserPreferences.Settings;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.UserPreferences;
+
+internal class SettingsViewModel : SubViewModel<SettingsWindowViewModel>
+{
+    public GeneralSettings General { get; set; } = new();
+
+    public ToolsSettings Tools { get; set; } = new();
+
+    public FileSettings File { get; set; } = new();
+
+    public UpdateSettings Update { get; set; } = new();
+
+    public DiscordSettings Discord { get; set; } = new();
+
+    public SettingsViewModel(SettingsWindowViewModel owner)
+        : base(owner)
+    {
+    }
+}

+ 53 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Dialogs/OptionPopup.axaml

@@ -0,0 +1,53 @@
+<Window
+    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:local="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
+    xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+    xmlns:markupExtensions="clr-namespace:PixiEditor.AvaloniaUI.Helpers.MarkupExtensions"
+    xmlns:panels="clr-namespace:PixiEditor.AvaloniaUI.Views.Panels"
+    mc:Ignorable="d"
+    d:DesignWidth="800"
+    d:DesignHeight="450"
+    x:Class="PixiEditor.AvaloniaUI.Views.Dialogs.OptionPopup"
+    Title="OptionPopup"
+    WindowStartupLocation="CenterScreen"
+    SizeToContent="WidthAndHeight"
+    Name="popup"
+    Background="{DynamicResource AccentColor}"
+    FlowDirection="{markupExtensions:Localization FlowDirection}"
+    ui:Translator.Key="{Binding #popup.Title}">
+    
+    <!--<WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>-->
+
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition/>
+            <RowDefinition Height="Auto"/>
+        </Grid.RowDefinitions>
+
+        <local:DialogTitleBar TitleKey="{Binding #popup.Title}" CloseCommand="{Binding #popup.CancelCommand}"/>
+
+        <ContentPresenter Content="{Binding #popup.PopupContent}"
+                          Grid.Row="1" Margin="15"/>
+
+        <ItemsControl ItemsSource="{Binding #popup.Options}" Grid.Row="2" Margin="15">
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <Button Content="{Binding}" MinWidth="70" Margin="5,0"
+                            Command="{Binding #popup.CloseCommand}" CommandParameter="{Binding}"/>
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+            <ItemsControl.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <panels:AlignableWrapPanel HorizontalAlignment="Center" HorizontalContentAlignment="Center"/>
+                </ItemsPanelTemplate>
+            </ItemsControl.ItemsPanel>
+        </ItemsControl>
+    </Grid>
+</Window>

+ 70 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Dialogs/OptionPopup.axaml.cs

@@ -0,0 +1,70 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using CommunityToolkit.Mvvm.Input;
+
+namespace PixiEditor.AvaloniaUI.Views.Dialogs;
+
+public partial class OptionPopup : Window
+{
+    public static readonly StyledProperty<object> PopupContentProperty =
+        AvaloniaProperty.Register<OptionPopup, object>(nameof(PopupContent));
+
+    public object PopupContent
+    {
+        get => GetValue(PopupContentProperty);
+        set => SetValue(PopupContentProperty, value);
+    }
+
+    public static readonly StyledProperty<ObservableCollection<object>> OptionsProperty =
+        AvaloniaProperty.Register<OptionPopup, ObservableCollection<object>>(nameof(Options));
+
+    public ObservableCollection<object> Options
+    {
+        get => GetValue(OptionsProperty);
+        set => SetValue(OptionsProperty, value);
+    }
+
+    public static readonly StyledProperty<object?> ResultProperty =
+        AvaloniaProperty.Register<OptionPopup, object?>(nameof(Result));
+
+    public object? Result
+    {
+        get => GetValue(ResultProperty);
+        set => SetValue(ResultProperty, value);
+    }
+
+    public RelayCommand CancelCommand { get; set; }
+
+    public RelayCommand CloseCommand { get; set; }
+
+    public OptionPopup(string title, object content, ObservableCollection<object> options)
+    {
+        PopupContent = content;
+        Options = options;
+        CancelCommand = new RelayCommand(Cancel);
+        CloseCommand = new RelayCommand(Close);
+        InitializeComponent();
+        
+        Loaded += OptionPopup_Loaded;
+    }
+
+    private void OptionPopup_Loaded(object? sender, EventArgs e)
+    {
+        InvalidateVisual();
+    }
+
+    private void Cancel()
+    {
+        Result = null;
+        Close();
+    }
+
+    private void Close(object parameter)
+    {
+        Result = parameter;
+        Close();
+    }
+}
+

+ 6 - 6
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Dock/DocumentTemplate.axaml

@@ -18,12 +18,12 @@
                                             MouseDownCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseDownCommand}"
                                             MouseMoveCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseMoveCommand}"
                                             MouseUpCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseUpCommand}"
-                                            MiddleMouseClickedCommand="{Binding IoSubViewModel.PreviewMouseMiddleButtonCommand, Source={viewModels:MainVM}}"
-                                            Cursor="{Binding ToolsSubViewModel.ToolCursor, Source={viewModels:MainVM}}"
-                                            GridLinesVisible="{Binding ViewportSubViewModel.GridLinesEnabled, Source={viewModels:MainVM}}"
-                                            ZoomMode="{Binding ToolsSubViewModel.ActiveTool, Source={viewModels:MainVM}, Converter={converters:ActiveToolToZoomModeConverter}}"
-                                            ZoomOutOnClick="{Binding ToolsSubViewModel.ZoomTool.ZoomOutOnClick, Source={viewModels:MainVM}}"
-                                            UseTouchGestures="{Binding StylusSubViewModel.UseTouchGestures, Source={viewModels:MainVM}}"
+                                            MiddleMouseClickedCommand="{Binding IoSubViewModel.PreviewMouseMiddleButtonCommand, Source={viewModels1:MainVM}}"
+                                            Cursor="{Binding ToolsSubViewModel.ToolCursor, Source={viewModels1:MainVM}}"
+                                            GridLinesVisible="{Binding ViewportSubViewModel.GridLinesEnabled, Source={viewModels1:MainVM}}"
+                                            ZoomMode="{Binding ToolsSubViewModel.ActiveTool, Source={viewModels1:MainVM}, Converter={converters:ActiveToolToZoomModeConverter}}"
+                                            ZoomOutOnClick="{Binding ToolsSubViewModel.ZoomTool.ZoomOutOnClick, Source={viewModels1:MainVM}}"
+                                            UseTouchGestures="{Binding StylusSubViewModel.UseTouchGestures, Source={viewModels1:MainVM}}"
                                             FlipX="{Binding FlipX, Mode=TwoWay}"
                                             FlipY="{Binding FlipY, Mode=TwoWay}"
                                             ContextRequested="Viewport_OnContextMenuOpening"

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

@@ -21,6 +21,17 @@ internal partial class MainWindow : Window
     private static ExtensionLoader extLoader;
 
     public new ViewModelMain DataContext { get => (ViewModelMain)base.DataContext; set => base.DataContext = value; }
+    
+    public static MainWindow? Current {
+        get 
+        {
+            if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+                return desktop.MainWindow as MainWindow;
+            if (Application.Current is null)
+                return null;
+            throw new NotSupportedException("ApplicationLifetime is not supported");
+        }
+    }
 
     public MainWindow(ExtensionLoader extensionLoader)
     {

+ 67 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ImportShortcutTemplatePopup.axaml

@@ -0,0 +1,67 @@
+<Window
+    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:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+    xmlns:markupExtensions="clr-namespace:PixiEditor.AvaloniaUI.Helpers.MarkupExtensions"
+    xmlns:dialogs="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
+    xmlns:viewModels="clr-namespace:PixiEditor.AvaloniaUI.ViewModels"
+    xmlns:shortcuts="clr-namespace:PixiEditor.AvaloniaUI.Views.Shortcuts"
+    mc:Ignorable="d"
+    d:DesignWidth="800"
+    d:DesignHeight="450"
+    x:Class="PixiEditor.AvaloniaUI.Views.Shortcuts.ImportShortcutTemplatePopup"
+    WindowStartupLocation="CenterOwner"
+    MinWidth="580"
+    SizeToContent="WidthAndHeight"
+    Background="{DynamicResource AccentColor}"
+    Name="window"
+    FlowDirection="{markupExtensions:Localization FlowDirection}"
+    ui:Translator.Key="IMPORT_FROM_TEMPLATE">
+
+    
+    <!--TODO figure out what these are for <Window.CommandBindings>
+        <CommandBinding
+            Command="{x:Static SystemCommands.CloseWindowCommand}"
+            CanExecute="CommandBinding_CanExecute"
+            Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>-->
+
+    <!--<WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32" GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>-->
+
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="32" />
+            <RowDefinition Height="*" />
+        </Grid.RowDefinitions>
+        <dialogs:DialogTitleBar
+            DockPanel.Dock="Top"
+            TitleKey="IMPORT_FROM_TEMPLATE"
+            CloseCommand="{x:Static viewModels:SystemCommands.CloseWindowCommand}" />
+        <ItemsControl
+            Grid.Row="1"
+            ItemsSource="{Binding Templates, ElementName=window}"
+            Margin="10,10,10,5">
+            <ItemsControl.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <StackPanel Orientation="Horizontal" />
+                </ItemsPanelTemplate>
+            </ItemsControl.ItemsPanel>
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <shortcuts:ShortcutsTemplateCard
+                        TemplateName="{Binding Name}"
+                        Margin="0 0 5 0"
+                        Logo="{Binding LogoPath}"
+                        Cursor="Hand"
+                        PointerReleased="OnTemplateCardPointerReleased"
+                        HoverLogo="{Binding Path=HoverLogoPath}" />
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+        </ItemsControl>
+    </Grid>
+</Window>

+ 122 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ImportShortcutTemplatePopup.axaml.cs

@@ -0,0 +1,122 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input;
+using PixiEditor.AvaloniaUI.Exceptions;
+using PixiEditor.AvaloniaUI.Models.Commands;
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using PixiEditor.AvaloniaUI.Models.Commands.Templates;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Views.Shortcuts;
+
+internal partial class ImportShortcutTemplatePopup : Window
+{
+    public IEnumerable<ShortcutProvider> Templates { get; set; }
+
+    public ImportShortcutTemplatePopup()
+    {
+        Templates = ShortcutProvider.GetProviders();
+        InitializeComponent();
+        
+        LayoutUpdated += (_, _) =>
+        {
+            MinHeight = Bounds.Height;
+        };
+    }
+
+    [Command.Internal("PixiEditor.Shortcuts.Provider.ImportDefault")]
+    public static void ImportDefaults(ShortcutProvider provider)
+    {
+        if (provider is not IShortcutDefaults defaults)
+        {
+            throw new ArgumentException("Provider must implement IShortcutDefaults", nameof(provider));
+        }
+
+        CommandController.Current.ResetShortcuts();
+        CommandController.Current.Import(defaults.DefaultShortcuts);
+
+        Success(provider);
+    }
+
+    [Command.Internal("PixiEditor.Shortcuts.Provider.ImportInstallation")]
+    public static void ImportInstallation(ShortcutProvider provider)
+    {
+        if (provider is not IShortcutInstallation defaults)
+        {
+            throw new ArgumentException("Provider must implement IShortcutInstallation", nameof(provider));
+        }
+
+        CommandController.Current.ResetShortcuts();
+
+        try
+        {
+            CommandController.Current.Import(defaults.GetInstalledShortcuts().Shortcuts);
+        }
+        catch (RecoverableException e)
+        {
+            NoticeDialog.Show(e.DisplayMessage, "ERROR");
+            return;
+        }
+
+        Success(provider);
+    }
+
+    private static void Success(ShortcutProvider provider) => NoticeDialog.Show(new LocalizedString("SHORTCUTS_IMPORTED", provider.Name), "SUCCESS");
+
+    // TODO figure out what these are for
+    /*
+    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }*/
+
+
+    /// <summary>
+    ///     Imports shortcuts from a provider. If provider has installation available, then user will be asked to choose between installation and defaults.
+    /// </summary>
+    /// <param name="provider">Shortcut provider.</param>
+    /// <returns>True if imported shortcuts.</returns>
+    private async Task<bool> ImportFromProvider(ShortcutProvider? provider)
+    {
+        if (provider is null)
+            return false;
+        if (provider.ProvidesFromInstallation && provider.HasInstallationPresent)
+        {
+            OptionsDialog<string> dialog = new(provider.Name, new LocalizedString("SHORTCUT_PROVIDER_DETECTED"), MainWindow.Current!)
+            {
+                { new LocalizedString("IMPORT_INSTALLATION_OPTION1"), x => ImportInstallation(provider) },
+                { new LocalizedString("IMPORT_INSTALLATION_OPTION2"), x => ImportDefaults(provider) },
+            };
+
+            return await dialog.ShowDialog();
+        }
+        
+        if (provider.HasDefaultShortcuts)
+        {
+            ImportDefaults(provider);
+            return true;
+        }
+        
+        return false;
+    }
+
+    private async void OnTemplateCardPointerReleased(object? sender, PointerReleasedEventArgs e)
+    {
+        if (e.InitialPressMouseButton != MouseButton.Left)
+            return;
+        ShortcutsTemplateCard? card = (ShortcutsTemplateCard?)sender;
+        ShortcutProvider? provider = card?.DataContext as ShortcutProvider;
+
+        if (await ImportFromProvider(provider))
+        {
+            Close();
+        }
+    }
+}

+ 46 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ShortcutsTemplateCard.axaml

@@ -0,0 +1,46 @@
+<UserControl
+    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"
+    mc:Ignorable="d"
+    d:DesignWidth="800"
+    d:DesignHeight="450"
+    x:Class="PixiEditor.AvaloniaUI.Views.Shortcuts.ShortcutsTemplateCard">
+    <Border BorderThickness="1" Height="150" Width="150" Background="{StaticResource MainColor}" 
+            CornerRadius="15" PointerEntered="OnBorderPointerEntered" PointerExited="OnBorderPointerExited">
+        <!-- TODO <Border.Triggers>
+            <EventTrigger RoutedEvent="Border.MouseEnter">
+                <BeginStoryboard>
+                    <Storyboard>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Width" 
+                                         From="72" To="100" Duration="0:0:0.15"/>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Height" 
+                                         From="72" To="100" Duration="0:0:0.15" />
+                    </Storyboard>
+                </BeginStoryboard>
+            </EventTrigger>
+            <EventTrigger RoutedEvent="Border.MouseLeave">
+                <BeginStoryboard>
+                    <Storyboard>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Width" 
+                                         From="100" To="72" Duration="0:0:0.15"/>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Height" 
+                                         From="100" To="72" Duration="0:0:0.15" />
+                    </Storyboard>
+                </BeginStoryboard>
+            </EventTrigger>
+        </Border.Triggers>-->
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*"/>
+                <RowDefinition Height="30"/>
+            </Grid.RowDefinitions>
+            <Image Grid.Row="0" Grid.RowSpan="2" Name="img" HorizontalAlignment="Center" VerticalAlignment="Center" Height="72" Width="72" Source="{Binding ElementName=card, Path=Logo}"/>
+            <Label 
+                Grid.Row="1" HorizontalAlignment="Center" FontWeight="Bold" Margin="0" Padding="0"
+                Content="{Binding ElementName=card, Path=TemplateName}"/>
+                <!--Style="{StaticResource BaseLabel}"-->
+        </Grid>
+    </Border>
+</UserControl>

+ 64 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Shortcuts/ShortcutsTemplateCard.axaml.cs

@@ -0,0 +1,64 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Styling;
+
+namespace PixiEditor.AvaloniaUI.Views.Shortcuts;
+
+public partial class ShortcutsTemplateCard : UserControl
+{
+    public static readonly StyledProperty<string> TemplateNameProperty = 
+        AvaloniaProperty.Register<ShortcutsTemplateCard, string>(nameof(TemplateName));
+
+    public string TemplateName
+    {
+        get { return (string)GetValue(TemplateNameProperty); }
+        set { SetValue(TemplateNameProperty, value); }
+    }
+
+    public static readonly StyledProperty<string>  LogoProperty = 
+        AvaloniaProperty.Register<ShortcutsTemplateCard, string>(nameof(Logo));
+
+    public static readonly StyledProperty<string>  HoverLogoProperty = 
+        AvaloniaProperty.Register<ShortcutsTemplateCard, string>(nameof(HoverLogo));
+
+    public string HoverLogo
+    {
+        get { return (string)GetValue(HoverLogoProperty); }
+        set { SetValue(HoverLogoProperty, value); }
+    }
+    
+    public string Logo
+    {
+        get { return (string)GetValue(LogoProperty); }
+        set { SetValue(LogoProperty, value); }
+    }
+    
+    public ShortcutsTemplateCard()
+    {
+        InitializeComponent();
+    }
+
+    private void OnBorderPointerExited(object? sender, PointerEventArgs e)
+    {
+        if (string.IsNullOrEmpty(HoverLogo))
+        {
+            return;
+        }
+        
+        img.Source = new Bitmap(HoverLogo);
+    }
+
+    private void OnBorderPointerEntered(object? sender, PointerEventArgs e)
+    {
+        if (string.IsNullOrEmpty(HoverLogo))
+        {
+            return;
+        }
+        
+        img.Source = new Bitmap(Logo);
+    }
+}
+

+ 290 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Windows/SettingsWindow.axaml

@@ -0,0 +1,290 @@
+<Window
+    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:cmds="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
+    xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+    xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+    xmlns:sys="clr-namespace:System;assembly=System.Runtime"
+    xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+    xmlns:vm="clr-namespace:PixiEditor.AvaloniaUI.ViewModels"
+    xmlns:ui1="clr-namespace:PixiEditor.AvaloniaUI.Helpers.UI"
+    xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
+    xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
+    xmlns:markupExtensions="clr-namespace:PixiEditor.AvaloniaUI.Helpers.MarkupExtensions"
+    xmlns:preferences="clr-namespace:PixiEditor.AvaloniaUI.Models.Preferences"
+    xmlns:dialogs="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
+    mc:Ignorable="d"
+    x:Class="PixiEditor.AvaloniaUI.Views.Windows.SettingsWindow"
+    Name="window"
+    d:DesignWidth="780" d:DesignHeight="688"
+    Width="780" Height="688"
+    MinWidth="665" MinHeight="500"
+    DataContext="{DynamicResource SettingsWindowViewModel}"
+    WindowStartupLocation="CenterScreen"
+    BorderBrush="Black" BorderThickness="1"
+    Background="{DynamicResource AccentColor}"
+    FlowDirection="{markupExtensions:Localization FlowDirection}"
+    ui:Translator.Key="SETTINGS">
+    
+    <Window.Resources>
+        <vm:SettingsWindowViewModel x:Key="SettingsWindowViewModel"/>
+    </Window.Resources>
+    
+    <!-- TODO
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+    -->
+    
+    <DockPanel Focusable="True" Background="{DynamicResource ThemeBackgroundBrush1}">
+        <!--Background="{StaticResource MainColor}"-->
+        <i:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </i:Interaction.Behaviors>
+
+        <dialogs:DialogTitleBar DockPanel.Dock="Top"
+            TitleKey="SETTINGS" CloseCommand="{x:Static vm:SystemCommands.CloseWindowCommand}"/>
+
+        <ListBox DockPanel.Dock="Left" x:Name="pages" ItemsSource="{Binding Pages}"
+                 Background="Transparent" BorderThickness="0" MinWidth="165"
+                 SelectedIndex="{Binding CurrentPage}">
+            <!--ItemContainerStyle="{StaticResource PixiListBoxItemStyle}"-->
+            <!--Width="Auto"-->
+            <ListBox.ItemTemplate>
+                <DataTemplate>
+                    <TextBlock Margin="10 5 10 5" FontSize="15" Foreground="White" Text="{Binding Path=Name.Value}"/>
+                </DataTemplate>
+            </ListBox.ItemTemplate>
+        </ListBox>
+        <StackPanel Orientation="Vertical">
+            <!--Background="{StaticResource AccentColor}"-->
+            <StackPanel Orientation="Vertical" Margin="15,10" Tag="27">
+                <StackPanel.IsVisible>
+                    <Binding Path="CurrentPage" Converter="{converters:IsEqualConverter}">
+                        <Binding.ConverterParameter><sys:Int32>0</sys:Int32></Binding.ConverterParameter>
+                    </Binding>
+                </StackPanel.IsVisible>
+                <Label ui:Translator.Key="LANGUAGE"/>
+                <!-- Style="{StaticResource SettingsHeader}"-->
+                <ComboBox Margin="27 5" Width="200" Height="25" FontSize="12" HorizontalAlignment="Left"
+                          ItemsSource="{Binding SettingsSubViewModel.General.AvailableLanguages}"
+                          SelectedItem="{Binding SettingsSubViewModel.General.SelectedLanguage, Mode=TwoWay}">
+                    <ComboBox.ItemTemplate>
+                        <DataTemplate>
+                            <StackPanel Orientation="Horizontal">
+                                <Image VerticalAlignment="Center" Margin="5 0" Source="{Binding IconFullPath}"/>
+                                <TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
+                            </StackPanel>
+                        </DataTemplate>
+                    </ComboBox.ItemTemplate>
+                    <!--<ComboBox.ItemContainerStyle>
+                        <Style TargetType="{x:Type ComboBoxItem}">
+                            <Setter Property="Template">
+                                <Setter.Value>
+                                    <ControlTemplate TargetType="{x:Type ComboBoxItem}">
+                                        <Border Height="25" Margin="0" Padding="5,0" BorderThickness="0,1">
+                                            <ContentPresenter/>
+                                            <Border.Style>
+                                                <Style TargetType="{x:Type Border}">
+                                                    <Style.Triggers>
+                                                        <Trigger Property="IsMouseOver" Value="False">
+                                                            <Setter Property="Background" Value="Transparent"/>
+                                                            <Setter Property="BorderBrush" Value="Transparent"/>
+                                                        </Trigger>
+                                                        <Trigger Property="IsMouseOver" Value="True">
+                                                            <Setter Property="Background" Value="{StaticResource MainColor}"/>
+                                                            <Setter Property="BorderBrush" Value="{StaticResource AlmostLightModeAccentColor}"/>
+                                                        </Trigger>
+                                                    </Style.Triggers>
+                                                </Style>
+                                            </Border.Style>
+                                        </Border>
+                                    </ControlTemplate>
+                                </Setter.Value>
+                            </Setter>
+                        </Style>
+                    </ComboBox.ItemContainerStyle>-->
+                </ComboBox>
+
+                <Label ui:Translator.Key="MISC" d:Content="Misc"/> <!--Styles="{StaticResource SettingsHeader}"-->
+
+                <CheckBox Margin="27 0"
+                          VerticalAlignment="Center" ui:Translator.Key="SHOW_STARTUP_WINDOW" d:Content="Show startup window"
+                          IsChecked="{Binding SettingsSubViewModel.File.ShowStartupWindow}"/>
+
+                <CheckBox Margin="27 10"
+                          VerticalAlignment="Center" ui:Translator.Key="DISABLE_NEWS_PANEL" d:Content="Hide news in startup window"
+                          IsChecked="{Binding SettingsSubViewModel.File.DisableNewsPanel}"/>
+
+                <CheckBox Margin="27 0"
+                          VerticalAlignment="Center" d:Content="Show image preview in taskbar" ui:Translator.Key="SHOW_IMAGE_PREVIEW_TASKBAR"
+                          IsChecked="{Binding SettingsSubViewModel.General.ImagePreviewInTaskbar}"/>
+
+                <StackPanel Margin="27 10 27 0" Orientation="Horizontal">
+                <Label 
+                    ui:Translator.Key="RECENT_FILE_LENGTH"
+                    ui:Translator.TooltipKey="RECENT_FILE_LENGTH_TOOLTIP"/>
+                    <!--Styles="{StaticResource SettingsText}"-->
+                    <input:NumberInput Margin="10 0 0 0" 
+                                              Min="0" FontSize="12" HorizontalAlignment="Left"
+                                   Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently, Mode=TwoWay}" Height="19" Width="40"/>
+                </StackPanel>
+
+                <Label
+                    d:Content="Default new file size" 
+                    ui:Translator.Key="DEFAULT_NEW_SIZE"/>
+                    <!--Styles="{StaticResource SettingsHeader}"-->
+
+                <StackPanel Orientation="Horizontal" Margin="27 5">
+                    <Label  d:Content="Width" ui:Translator.Key="WIDTH"/>
+                    <!--Styles="{StaticResource SettingsText}"-->
+                    <input:SizeInput Margin="10 0 0 0" 
+                                 Size="{Binding SettingsSubViewModel.File.DefaultNewFileWidth, Mode=TwoWay}" 
+                                 Height="21" MaxSize="9999" HorizontalAlignment="Left"/>
+                </StackPanel>
+
+                <StackPanel Orientation="Horizontal" Margin="27 5">
+                    <Label  d:Content="Height" ui:Translator.Key="HEIGHT"/> 
+                    <!--Styles="{StaticResource SettingsText}"-->
+                    <input:SizeInput Margin="7 0 0 0"
+                                 Size="{Binding SettingsSubViewModel.File.DefaultNewFileHeight, Mode=TwoWay}" 
+                                 Height="21" MaxSize="9999" HorizontalAlignment="Left"/>
+                </StackPanel>
+
+                <Label  d:Content="Tools" ui:Translator.Key="TOOLS"/>
+                <!--Styles="{StaticResource SettingsHeader}"-->
+                
+                <StackPanel Margin="27 0" Orientation="Horizontal">
+                    <Label Margin="0,0,7,0"
+                           ui:Translator.Key="RIGHT_CLICK_MODE"/>
+                    <!--Styles="{StaticResource SettingsText}"-->
+                    <ComboBox SelectedItem="{Binding RightClickMode, Source={vm:MainVM ToolsSVM}, Mode=TwoWay}"
+                              ItemsSource="{markupExtensions:Enum preferences:RightClickMode}"
+                              Width="160"/>
+                    <!--Styles="{StaticResource TranslatedEnum}"-->
+                </StackPanel>
+
+                <CheckBox VerticalAlignment="Center" Margin="27 5"
+                    IsChecked="{Binding SettingsSubViewModel.Tools.EnableSharedToolbar}" d:Content="Enable shared toolbar" ui:Translator.Key="ENABLE_SHARED_TOOLBAR"/>
+
+                <Label  d:Content="Automatic updates" ui:Translator.Key="AUTOMATIC_UPDATES"/>
+                <!--Styles="{StaticResource SettingsHeader}"-->
+
+                <CheckBox Margin="27 5" VerticalAlignment="Center" IsEnabled="{Binding Path=ShowUpdateTab}"
+                    IsChecked="{Binding SettingsSubViewModel.Update.CheckUpdatesOnStartup}" ui:Translator.Key="CHECK_FOR_UPDATES" d:Content="Check updates on startup"/>
+
+                <StackPanel Orientation="Horizontal" Margin="27 5">
+                    <Label Grid.Row="11" Grid.Column="1" d:Content="Update stream" ui:Translator.Key="UPDATE_STREAM"/>
+                    <!--Styles="{StaticResource SettingsText}"-->
+                    <StackPanel Margin="5 0" Orientation="Horizontal" VerticalAlignment="Center"
+                            Height="21.96" HorizontalAlignment="Left">
+                <ComboBox Width="110" IsEnabled="{Binding Path=ShowUpdateTab}"
+                    ItemsSource="{Binding SettingsSubViewModel.Update.UpdateChannels}"
+                    SelectedValue="{Binding SettingsSubViewModel.Update.UpdateChannelName}"/>
+                <Image Cursor="Help" Margin="10 0 0 0" Source="/Images/Commands/PixiEditor/Links/OpenDocumentation.png"
+                       IsVisible="{Binding !ShowUpdateTab}"
+                       ui:Translator.TooltipKey="UPDATE_CHANNEL_HELP_TOOLTIP"/>
+                       <!-- ToolTipService.InitialShowDelay="0"-->
+                </StackPanel>
+                </StackPanel>
+
+                <Label d:Content="Debug" ui:Translator.Key="DEBUG"/>
+                <!--Styles="{StaticResource SettingsHeader}"-->
+                <CheckBox Margin="27 5" VerticalAlignment="Center"
+                    IsChecked="{Binding SettingsSubViewModel.General.IsDebugModeEnabled}" ui:Translator.Key="ENABLE_DEBUG_MODE" d:Content="Enable Debug Mode"/>
+                <!--<Label Margin="0 5" Styles="{StaticResource SettingsText}" VerticalAlignment="Center">
+                    <ui1:Hyperlink Command="{cmds:Command PixiEditor.Debug.OpenCrashReportsDirectory}" Style="{StaticResource SettingsLink}">
+                        <Run ui:Translator.Key="OPEN_CRASH_REPORTS_DIR" d:Text="Open crash reports directory"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </ui1:Hyperlink>
+                </Label>-->
+            </StackPanel>
+
+            <StackPanel Margin="15,10">
+                <StackPanel.IsVisible>
+                    <Binding Path="CurrentPage" Converter="{converters:IsEqualConverter}">
+                        <Binding.ConverterParameter>
+                            <sys:Int32>1</sys:Int32>
+                        </Binding.ConverterParameter>
+                    </Binding>
+                </StackPanel.IsVisible>
+                <StackPanel Orientation="Vertical">
+                    <Label d:Content="Rich Presence" ui:Translator.Key="DISCORD_RICH_PRESENCE"/>
+                    <!--Styles="{StaticResource SettingsHeader}"-->
+
+                    <CheckBox Margin="27 5" VerticalAlignment="Center"
+                              IsChecked="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" d:Content="Enabled" ui:Translator.Key="ENABLED"/>
+                    <CheckBox Margin="27 5" VerticalAlignment="Center"
+                              IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" 
+                              IsChecked="{Binding SettingsSubViewModel.Discord.ShowDocumentName}" d:Content="Show image name" ui:Translator.Key="SHOW_IMAGE_NAME"/>
+                    <CheckBox Margin="27 5" VerticalAlignment="Center"
+                              IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" d:Content="Show image size" ui:Translator.Key="SHOW_IMAGE_SIZE"
+                              IsChecked="{Binding SettingsSubViewModel.Discord.ShowDocumentSize}"/>
+                    <CheckBox Margin="27 5" VerticalAlignment="Center"
+                              IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" ui:Translator.Key="SHOW_LAYER_COUNT" d:Content="Show layer count"
+                              IsChecked="{Binding SettingsSubViewModel.Discord.ShowLayerCount}"/>
+                </StackPanel>
+                <!--<usercontrols:DiscordRPPreview 
+                    Margin="15"
+                    Width="280"
+                    State="{Binding SettingsSubViewModel.Discord.StatePreview}" 
+                    Detail="{Binding SettingsSubViewModel.Discord.DetailPreview}" 
+                    IsPlaying="{Binding SettingsSubViewModel.Discord.EnableRichPresence}"/>-->
+            </StackPanel>
+
+            <Grid Height="{Binding ElementName=window, Path=Height, Converter={converters:SubtractConverter}, ConverterParameter=50}"
+                  Margin="10,10,10,50">
+                <Grid.IsVisible>
+                    <Binding Path="CurrentPage" Converter="{converters:IsEqualConverter}">
+                        <Binding.ConverterParameter>
+                            <sys:Int32>2</sys:Int32>
+                        </Binding.ConverterParameter>
+                    </Binding>
+                </Grid.IsVisible>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto"/>
+                    <RowDefinition Height="Auto"/>
+                    <RowDefinition/>
+                </Grid.RowDefinitions>
+                <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
+                    <!--<StackPanel.Resources>
+                        <Style TargetType="Button" BasedOn="{StaticResource DarkRoundButton}">
+                            <Setter Property="HorizontalAlignment" Value="Stretch"/>
+                            <Setter Property="MinWidth" Value="115"/>
+                            <Setter Property="Height" Value="Auto"/>
+                            <Setter Property="Margin" Value="5,0"/>
+                            <Setter Property="FontSize" Value="12"/>
+                            <Setter Property="Padding" Value="5"/>
+                        </Style>
+                    </StackPanel.Resources>-->
+                    <Button Command="{cmds:Command PixiEditor.Shortcuts.Export}"
+                            d:Content="Export" ui:Translator.Key="EXPORT"/>
+                    <Button Command="{cmds:Command PixiEditor.Shortcuts.Import}"
+                            d:Content="Import" ui:Translator.Key="IMPORT"/>
+                    <Button Command="{cmds:Command PixiEditor.Shortcuts.OpenTemplatePopup}"
+                            d:Content="Shortcut Templates" ui:Translator.Key="SHORTCUT_TEMPLATES"/>
+                    <Button Command="{cmds:Command PixiEditor.Shortcuts.Reset}"
+                            d:Content="Reset all" ui:Translator.Key="RESET_ALL"/>
+                </StackPanel>
+                <TextBox Grid.Row="1" Margin="0,10"
+                         Text="{Binding SearchTerm, Mode=TwoWay}">
+                        <!--Styles="{StaticResource DarkTextBoxStyle}"-->
+                    <i:Interaction.Behaviors>
+                        <behaviours:GlobalShortcutFocusBehavior/>
+                    </i:Interaction.Behaviors>
+                </TextBox>
+
+                <!--<settingGroups:ShortcutsBinder Grid.Row="2"/>-->
+            </Grid>
+        </StackPanel>
+    </DockPanel>
+    
+</Window>

+ 29 - 0
src/PixiEditor.Avalonia/PixiEditor.AvaloniaUI/Views/Windows/SettingsWindow.axaml.cs

@@ -0,0 +1,29 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using PixiEditor.AvaloniaUI.ViewModels;
+
+namespace PixiEditor.AvaloniaUI.Views.Windows;
+
+public partial class SettingsWindow : Window
+{
+    public SettingsWindow(int page = 0)
+    {
+        InitializeComponent();
+        var viewModel = DataContext as SettingsWindowViewModel;
+        viewModel!.CurrentPage = page;
+    }
+
+    //TODO figure out what's the purpose of this
+    /*
+    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }*/
+}
+

+ 2 - 0
src/PixiEditor.sln.DotSettings

@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 2 - 1
src/PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -23,7 +23,8 @@
         Name="window" 
         Height="688" Width="780"
         MinHeight="500" MinWidth="665"
-        WindowStyle="None" DataContext="{DynamicResource SettingsWindowViewModel}"
+        WindowStyle="None" 
+        DataContext="{DynamicResource SettingsWindowViewModel}"
         WindowStartupLocation="CenterScreen"
         BorderBrush="Black" BorderThickness="1"
         Background="{StaticResource AccentColor}"