Pārlūkot izejas kodu

Ported entry logic

Krzysztof Krysiński 2 gadi atpakaļ
vecāks
revīzija
55ee17ef16
17 mainītis faili ar 926 papildinājumiem un 30 dzēšanām
  1. 2 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/App.axaml.cs
  2. 83 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/ServiceCollectionHelpers.cs
  3. 4 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/CommandCollection.cs
  4. 1 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/CommandController.cs
  5. 1 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/Commands/Command.cs
  6. 5 4
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Command.cs
  7. 2 5
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/ContextMenu.cs
  8. 5 10
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Menu.cs
  9. 1 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/IToolsHandler.cs
  10. 253 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Preferences/PreferencesSettings.cs
  11. 163 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/UserData/RecentlyOpenedDocument.cs
  12. 32 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/MainViewModel.cs
  13. 16 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/ToolsViewModel.cs
  14. 33 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SystemCommands.cs
  15. 292 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Main/MainTitleBar.axaml
  16. 31 2
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/MainWindow.axaml.cs
  17. 2 2
      src/PixiEditor.Extensions/UI/Translator.cs

+ 2 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/App.axaml.cs

@@ -24,7 +24,8 @@ public partial class App : Application
         }
         else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
         {
-            singleViewPlatform.MainView = new MainView { DataContext = new MainViewModel() };
+            throw new NotImplementedException();
+            //singleViewPlatform.MainView = new MainView { DataContext = new MainViewModel() };
         }
 
         base.OnFrameworkInitializationCompleted();

+ 83 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/ServiceCollectionHelpers.cs

@@ -0,0 +1,83 @@
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Extensions.Windowing;
+using PixiEditor.Models.AppExtensions;
+using PixiEditor.Models.AppExtensions.Services;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Containers;
+using PixiEditor.Models.Localization;
+using PixiEditor.Models.Preferences;
+using PixiEditor.ViewModels.SubViewModels;
+using PixiEditor.ViewModels.SubViewModels.AdditionalContent;
+
+namespace PixiEditor.Helpers.Extensions;
+
+internal static class ServiceCollectionHelpers
+{
+    /// <summary>
+    /// Adds all the services required to fully run PixiEditor's MainWindow
+    /// </summary>
+    public static IServiceCollection
+        AddPixiEditor(this IServiceCollection collection, ExtensionLoader extensionLoader) => collection
+        .AddSingleton<MainViewModel>()
+        .AddSingleton<IPreferences, PreferencesSettings>()
+        .AddSingleton<ILocalizationProvider, LocalizationProvider>(x => new LocalizationProvider(extensionLoader))
+
+        // View Models
+        .AddSingleton<ToolsViewModel>()
+        .AddSingleton<IToolsHandler, ToolsViewModel>()
+        /*.AddSingleton<StylusViewModel>()
+        .AddSingleton<WindowViewModel>()
+        .AddSingleton<FileViewModel>()
+        .AddSingleton<UpdateViewModel>()
+        .AddSingleton<IoViewModel>()
+        .AddSingleton<LayersViewModel>()
+        .AddSingleton<ClipboardViewModel>()
+        .AddSingleton<UndoViewModel>()
+        .AddSingleton<SelectionViewModel>()
+        .AddSingleton<ViewOptionsViewModel>()
+        .AddSingleton<ColorsViewModel>()
+        .AddSingleton<RegistryViewModel>()
+        .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
+        .AddSingleton<DebugViewModel>()
+        .AddSingleton<SearchViewModel>()*/
+        .AddSingleton<AdditionalContentViewModel>()
+        //.AddSingleton(x => new ExtensionsViewModel(x.GetService<ViewModelMain>(), extensionLoader))
+        // Controllers
+        //.AddSingleton<ShortcutController>()
+        .AddSingleton<CommandController>();
+        /*.AddSingleton<DocumentManagerViewModel>()
+        // Tools
+        .AddSingleton<ToolViewModel, MoveViewportToolViewModel>()
+        .AddSingleton<ToolViewModel, RotateViewportToolViewModel>()
+        .AddSingleton<ToolViewModel, MoveToolViewModel>()
+        .AddSingleton<ToolViewModel, PenToolViewModel>()
+        .AddSingleton<ToolViewModel, SelectToolViewModel>()
+        .AddSingleton<ToolViewModel, MagicWandToolViewModel>()
+        .AddSingleton<ToolViewModel, LassoToolViewModel>()
+        .AddSingleton<ToolViewModel, FloodFillToolViewModel>()
+        .AddSingleton<ToolViewModel, LineToolViewModel>()
+        .AddSingleton<ToolViewModel, EllipseToolViewModel>()
+        .AddSingleton<ToolViewModel, RectangleToolViewModel>()
+        .AddSingleton<ToolViewModel, EraserToolViewModel>()
+        .AddSingleton<ToolViewModel, ColorPickerToolViewModel>()
+        .AddSingleton<ToolViewModel, BrightnessToolViewModel>()
+        .AddSingleton<ToolViewModel, ZoomToolViewModel>()
+        // Palette Parsers
+        .AddSingleton<PaletteFileParser, JascFileParser>()
+        .AddSingleton<PaletteFileParser, ClsFileParser>()
+        .AddSingleton<PaletteFileParser, PngPaletteParser>()
+        .AddSingleton<PaletteFileParser, PaintNetTxtParser>()
+        .AddSingleton<PaletteFileParser, HexPaletteParser>()
+        .AddSingleton<PaletteFileParser, GimpGplParser>()
+        .AddSingleton<PaletteFileParser, PixiPaletteParser>()
+        // Palette data sources
+        .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>();*/
+
+    public static IServiceCollection AddExtensionServices(this IServiceCollection collection) =>
+        collection.AddSingleton<IWindowProvider, WindowProvider>()
+            .AddSingleton<IPaletteProvider, PaletteProvider>();
+}

+ 4 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/CommandCollection.cs

@@ -3,7 +3,9 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Windows.Input;
 using Avalonia.Input;
+using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using Command = PixiEditor.Models.Commands.Commands.Command;
 
 namespace PixiEditor.Models.Commands;
@@ -18,7 +20,8 @@ internal class CommandCollection : ICollection<Command>
 
     public bool IsReadOnly => false;
 
-    public Command this[string name] => _commandInternalNames[name];
+    public Command this[string name] => new Command.BasicCommand(
+        (_) => NoticeDialog.Show("Debug", "Debug"), new CanExecuteEvaluator() { Evaluate = (_) => true });//_commandInternalNames[name];
     public bool ContainsKey(string key) => _commandInternalNames.ContainsKey(key);
 
     public List<Command> this[KeyCombination shortcut] => _commandShortcuts[shortcut];

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/CommandController.cs

@@ -146,7 +146,7 @@ internal class CommandController
     private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, LocalizedString displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,
         ShortcutsTemplate template)
     {
-        IToolsHandler toolsHandler = serviceProvider.GetRequiredService<IToolsHandler>();
+        IToolsHandler toolsHandler = serviceProvider.GetService<IToolsHandler>();
         foreach (var toolInstance in serviceProvider.GetServices<IToolHandler>())
         {
             var type = toolInstance.GetType();

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/Commands/Command.cs

@@ -69,7 +69,7 @@ internal abstract partial class Command : ReactiveObject
 
     public bool CanExecute() => Methods.CanExecute(GetParameter());
 
-    public IImage GetIcon() => IconEvaluator.CallEvaluate(this, GetParameter());
+    public IImage GetIcon() => IconEvaluator == null ? null : IconEvaluator.CallEvaluate(this, GetParameter());
 
     public delegate void ShortcutChangedEventHandler(Command command, ShortcutChangedEventArgs args);
 }

+ 5 - 4
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Command.cs

@@ -41,7 +41,7 @@ internal class Command : MarkupExtension
 
         if (commandController is null)
         {
-            commandController = serviceProvider.GetRequiredService<CommandController>();
+            commandController = CommandController.Current; // TODO: Find a better way to get the current CommandController
         }
 
         var command = commandController.Commands[Name];
@@ -84,16 +84,17 @@ internal class Command : MarkupExtension
         {
             return this.WhenAnyValue(x => x.Command, x => x.UseProvidedParameter, (command, useProvidedParameter) =>
             {
-                if (useProvidedParameter)
+                return true;
+                /*if (useProvidedParameter)
                 {
                     return command.CanExecute();
-                    //return command.CanExecute(parameter); // Should be this, but idk how to make it properly, I think whole logic should be changed so it fits
+                    //TODO: return command.CanExecute(parameter); // Should be this, but idk how to make it properly, I think whole logic should be changed so it fits
                     // reactiveUI
                 }
                 else
                 {
                     return command.CanExecute();
-                }
+                }*/
             });
         }
 

+ 2 - 5
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/ContextMenu.cs

@@ -7,14 +7,11 @@ namespace PixiEditor.Models.Commands.XAML;
 
 internal class ContextMenu : global::Avalonia.Controls.ContextMenu
 {
-    public static readonly DirectProperty<ContextMenu, string> CommandNameProperty;
+    public static readonly StyledProperty<string> CommandNameProperty =
+        AvaloniaProperty.Register<Menu, string>(nameof(Command));
 
     static ContextMenu()
     {
-        CommandNameProperty = AvaloniaProperty.RegisterDirect<ContextMenu, string>(
-            nameof(Command),
-            GetCommand,
-            SetCommand);
         CommandNameProperty.Changed.Subscribe(CommandChanged);
     }
 

+ 5 - 10
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Menu.cs

@@ -8,22 +8,17 @@ namespace PixiEditor.Models.Commands.XAML;
 
 internal class Menu : global::Avalonia.Controls.Menu
 {
-    public static readonly DirectProperty<Menu, string> CommandNameProperty;
+    public static readonly AttachedProperty<string> CommandProperty =
+        AvaloniaProperty.RegisterAttached<Menu, MenuItem, string>(nameof(Command));
 
     static Menu()
     {
-        CommandNameProperty = AvaloniaProperty.RegisterDirect<Menu, string>(
-            nameof(Command),
-            GetCommand,
-            SetCommand);
-        CommandNameProperty.Changed.Subscribe(CommandChanged);
+        CommandProperty.Changed.Subscribe(CommandChanged);
     }
 
     public const double IconDimensions = 21;
-    
-    public static string GetCommand(Menu menu) => (string)menu.GetValue(CommandNameProperty);
-
-    public static void SetCommand(Menu menu, string value) => menu.SetValue(CommandNameProperty, value);
+    public static string GetCommand(MenuItem menu) => (string)menu.GetValue(CommandProperty);
+    public static void SetCommand(MenuItem menu, string value) => menu.SetValue(CommandProperty, value);
 
     public static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
     {

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/IToolsHandler.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.Models.Containers;
 
-internal  interface IToolsHandler
+internal interface IToolsHandler
 {
     public void SetTool(object parameter);
 }

+ 253 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Preferences/PreferencesSettings.cs

@@ -0,0 +1,253 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PixiEditor.Extensions.Common.UserPreferences;
+
+namespace PixiEditor.Models.Preferences;
+
+[DebuggerDisplay("{Preferences.Count + LocalPreferences.Count} Preference(s)")]
+internal class PreferencesSettings : IPreferences
+{
+    public bool IsLoaded { get; private set; } = false;
+
+    public string PathToRoamingUserPreferences { get; private set; } = GetPathToSettings(Environment.SpecialFolder.ApplicationData, "user_preferences.json");
+
+    public string PathToLocalPreferences { get; private set; } = GetPathToSettings(Environment.SpecialFolder.LocalApplicationData, "editor_data.json");
+
+    public Dictionary<string, object> Preferences { get; set; } = new Dictionary<string, object>();
+
+    public Dictionary<string, object> LocalPreferences { get; set; } = new Dictionary<string, object>();
+
+    public void Init()
+    {
+        IPreferences.SetAsCurrent(this);
+        Init(PathToRoamingUserPreferences, PathToLocalPreferences);
+    }
+
+    public void Init(string path, string localPath)
+    {
+        PathToRoamingUserPreferences = path;
+        PathToLocalPreferences = localPath;
+
+        if (IsLoaded == false)
+        {
+            Preferences = InitPath(path);
+            LocalPreferences = InitPath(localPath);
+
+            IsLoaded = true;
+        }
+    }
+
+    public void UpdatePreference<T>(string name, T value)
+    {
+        if (IsLoaded == false)
+        {
+            Init();
+        }
+
+        Preferences[name] = value;
+
+        if (Callbacks.ContainsKey(name))
+        {
+            foreach (var action in Callbacks[name])
+            {
+                action.Invoke(value);
+            }
+        }
+
+        Save();
+    }
+
+    public void UpdateLocalPreference<T>(string name, T value)
+    {
+        if (IsLoaded == false)
+        {
+            Init();
+        }
+
+        LocalPreferences[name] = value;
+
+        if (Callbacks.ContainsKey(name))
+        {
+            foreach (var action in Callbacks[name])
+            {
+                action.Invoke(value);
+            }
+        }
+
+        Save();
+    }
+
+    public void Save()
+    {
+        if (IsLoaded == false)
+        {
+            Init();
+        }
+
+        File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
+        File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+    }
+
+    public Dictionary<string, List<Action<object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<object>>>();
+
+    public void AddCallback(string name, Action<object> action)
+    {
+        if (action == null)
+        {
+            throw new ArgumentNullException(nameof(action));
+        }
+
+        if (Callbacks.ContainsKey(name))
+        {
+            Callbacks[name].Add(action);
+            return;
+        }
+
+        Callbacks.Add(name, new List<Action<object>>() { action });
+    }
+
+    public void AddCallback<T>(string name, Action<T> action)
+    {
+        if (action == null)
+        {
+            throw new ArgumentNullException(nameof(action));
+        }
+
+        AddCallback(name, new Action<object>(o => action((T)o)));
+    }
+
+    public void RemoveCallback(string name, Action<object> action)
+    {
+        if (action == null)
+        {
+            throw new ArgumentNullException(nameof(action));
+        }
+
+        if (Callbacks.TryGetValue(name, out var callback))
+        {
+            callback.Remove(action);
+        }
+    }
+
+    public void RemoveCallback<T>(string name, Action<T> action)
+    {
+        if (action == null)
+        {
+            throw new ArgumentNullException(nameof(action));
+        }
+
+        RemoveCallback(name, new Action<object>(o => action((T)o)));
+    }
+
+#nullable enable
+
+    public T? GetPreference<T>(string name)
+    {
+        return GetPreference(name, default(T));
+    }
+
+    public T? GetPreference<T>(string name, T? fallbackValue)
+    {
+        if (IsLoaded == false)
+        {
+            Init();
+        }
+
+        try
+        {
+            return GetValue(Preferences, name, fallbackValue);
+        }
+        catch (InvalidCastException)
+        {
+            Preferences.Remove(name);
+            Save();
+
+            return fallbackValue;
+        }
+    }
+
+    public T? GetLocalPreference<T>(string name)
+    {
+        return GetLocalPreference(name, default(T));
+    }
+
+    public T? GetLocalPreference<T>(string name, T? fallbackValue)
+    {
+        if (IsLoaded == false)
+        {
+            Init();
+        }
+
+        try
+        {
+            return GetValue(LocalPreferences, name, fallbackValue);
+        }
+        catch (InvalidCastException)
+        {
+            LocalPreferences.Remove(name);
+            Save();
+
+            return fallbackValue;
+        }
+    }
+
+    private T? GetValue<T>(Dictionary<string, object> dict, string name, T? fallbackValue)
+    {
+        if (!dict.ContainsKey(name)) return fallbackValue;
+        var preference = dict[name];
+        if (typeof(T) == preference.GetType()) return (T)preference;
+
+        if (typeof(T).IsEnum)
+        {
+            return (T)Enum.Parse(typeof(T), preference.ToString());
+        }
+        
+        if (preference.GetType() == typeof(JArray))
+        {
+            return ((JArray)preference).ToObject<T>();
+        }
+
+        return (T)Convert.ChangeType(dict[name], typeof(T));
+    }
+
+#nullable disable
+
+    private static string GetPathToSettings(Environment.SpecialFolder folder, string fileName)
+    {
+        return Path.Join(
+            Environment.GetFolderPath(folder),
+            "PixiEditor",
+            fileName);
+    }
+
+    private static Dictionary<string, object> InitPath(string path)
+    {
+        string dir = Path.GetDirectoryName(path);
+
+        if (!Directory.Exists(dir))
+        {
+            Directory.CreateDirectory(dir);
+        }
+
+        if (!File.Exists(path))
+        {
+            File.WriteAllText(path, "{\n}");
+        }
+        else
+        {
+            string json = File.ReadAllText(path);
+            var dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
+
+            // dictionary is null if the user deletes the content of the preference file.
+            if (dictionary != null)
+            {
+                return dictionary;
+            }
+        }
+
+        return new Dictionary<string, object>();
+    }
+}

+ 163 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/UserData/RecentlyOpenedDocument.cs

@@ -0,0 +1,163 @@
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using ChunkyImageLib;
+using PixiEditor.Avalonia.Exceptions.Exceptions;
+using PixiEditor.Parser.Deprecated;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Helpers;
+using PixiEditor.Parser;
+using ReactiveUI;
+
+namespace PixiEditor.Models.DataHolders;
+
+[DebuggerDisplay("{FilePath}")]
+internal class RecentlyOpenedDocument : ReactiveObject
+{
+    private bool corrupt;
+
+    private string filePath;
+
+    private SKBitmap previewBitmap;
+
+    public string FilePath
+    {
+        get => filePath;
+        set
+        {
+            this.RaiseAndSetIfChanged(ref filePath, value);
+            this.RaisePropertyChanged(nameof(FileName));
+            this.RaisePropertyChanged(nameof(FileExtension));
+            PreviewBitmap = null;
+        }
+    }
+
+    public bool Corrupt
+    {
+        get => corrupt;
+        set => this.RaiseAndSetIfChanged(ref corrupt, value);
+    }
+
+    public string FileName => Path.GetFileNameWithoutExtension(filePath);
+
+    public string FileExtension
+    {
+        get
+        {
+            if (!File.Exists(FilePath))
+            {
+                return "? (Not found)";
+            }
+
+            if (Corrupt)
+            {
+                return "? (Corrupt)";
+            }
+
+            string extension = Path.GetExtension(filePath).ToLower();
+            return SupportedFilesHelper.IsExtensionSupported(extension) ? extension : $"? ({extension})";
+        }
+    }
+
+    public SKBitmap PreviewBitmap
+    {
+        get
+        {
+            if (previewBitmap == null && !Corrupt)
+            {
+                previewBitmap = LoadPreviewBitmap();
+            }
+
+            return previewBitmap;
+        }
+        private set => this.RaiseAndSetIfChanged(ref previewBitmap, value);
+    }
+
+    public RecentlyOpenedDocument(string path)
+    {
+        FilePath = path;
+    }
+
+    private SKBitmap LoadPreviewBitmap()
+    {
+        if (!File.Exists(FilePath))
+        {
+            return null;
+        }
+
+        if (FileExtension == ".pixi")
+        {
+            SerializableDocument serializableDocument;
+
+            try
+            {
+                var document = PixiParser.Deserialize(filePath);
+
+                if (document.PreviewImage == null || document.PreviewImage.Length == 0)
+                {
+                    return null;
+                }
+
+                using var data = new MemoryStream(document.PreviewImage);
+                return SKBitmap.Decode(data);
+            }
+            catch
+            {
+
+                try
+                {
+                    serializableDocument = DepractedPixiParser.Deserialize(filePath);
+                }
+                catch
+                {
+                    corrupt = true;
+                    return null;
+                }
+            }
+
+            //TODO: Fix this
+            /*using Surface surface = Surface.Combine(serializableDocument.Width, serializableDocument.Height,
+                serializableDocument.Layers
+                    .Where(x => x.Opacity > 0.8)
+                    .Select(x => (x.ToImage(), new VecI(x.OffsetX, x.OffsetY))).ToList());
+
+            return DownscaleToMaxSize(surface.ToBitmap());*/
+
+            return null;
+        }
+
+        if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
+        {
+            SKBitmap bitmap = null;
+
+            try
+            {
+                //TODO: Fix this
+                //bitmap = Importer.ImportWriteableBitmap(FilePath);
+            }
+            catch (RecoverableException)
+            {
+                corrupt = true;
+                return null;
+            }
+
+            if (bitmap == null) //prevent crash
+                return null;
+
+            return DownscaleToMaxSize(bitmap);
+        }
+
+        return null;
+    }
+
+    private SKBitmap DownscaleToMaxSize(SKBitmap bitmap)
+    {
+        if (bitmap.Width > Constants.MaxPreviewWidth || bitmap.Height > Constants.MaxPreviewHeight)
+        {
+            double factor = Math.Min(Constants.MaxPreviewWidth / (double)bitmap.Width, Constants.MaxPreviewHeight / (double)bitmap.Height);
+            return bitmap.Resize(new SKSizeI((int)(bitmap.Width * factor), (int)(bitmap.Height * factor)), SKFilterQuality.High);
+        }
+
+        return bitmap;
+    }
+}

+ 32 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/MainViewModel.cs

@@ -1,18 +1,49 @@
 using System.Reactive;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Localization;
+using PixiEditor.Platform;
+using PixiEditor.ViewModels.SubViewModels;
 using ReactiveUI;
 
 namespace PixiEditor.Avalonia.ViewModels;
 
-public class MainViewModel : ViewModelBase
+internal class MainViewModel : ViewModelBase
 {
     public event Action OnStartupEvent;
+
+    public IServiceProvider Services { get; set; }
+    public CommandController CommandController { get; set; }
     public ReactiveCommand<Unit, Unit> OnStartupCommand { get; }
+    public ToolsViewModel ToolsViewModel { get; set; }
+
+    public IPreferences Preferences { get; set; }
+    public ILocalizationProvider LocalizationProvider { get; set; }
 
     public MainViewModel()
     {
         OnStartupCommand = ReactiveCommand.Create(OnStartup);
     }
 
+    public void Setup(IServiceProvider services)
+    {
+        Services = services;
+
+        Preferences = services.GetRequiredService<IPreferences>();
+        Preferences.Init();
+
+        LocalizationProvider = services.GetRequiredService<ILocalizationProvider>();
+        LocalizationProvider.LoadData();
+
+        ToolsViewModel = services.GetService<ToolsViewModel>();
+
+        CommandController = services.GetService<CommandController>();
+        CommandController.Init(services);
+    }
+
     private void OnStartup()
     {
         OnStartupEvent?.Invoke();

+ 16 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -0,0 +1,16 @@
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.Models.Containers;
+
+namespace PixiEditor.ViewModels.SubViewModels;
+
+internal class ToolsViewModel : SubViewModel<MainViewModel>, IToolsHandler
+{
+    public ToolsViewModel(MainViewModel owner) : base(owner)
+    {
+    }
+
+    public void SetTool(object parameter)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 33 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SystemCommands.cs

@@ -0,0 +1,33 @@
+using System.Reactive;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace PixiEditor.Avalonia.ViewModels;
+
+public static class SystemCommands
+{
+    public static ReactiveCommand<Window, Unit> CloseWindowCommand { get; } = ReactiveCommand.Create<Window>(CloseWindow);
+    public static ReactiveCommand<Window, Unit> MaximizeWindowCommand { get; } = ReactiveCommand.Create<Window>(MaximizeWindow);
+    public static ReactiveCommand<Window, Unit> MinimizeWindowCommand { get; } = ReactiveCommand.Create<Window>(MinimizeWindow);
+    public static ReactiveCommand<Window, Unit> RestoreWindowCommand { get; } = ReactiveCommand.Create<Window>(RestoreWindow);
+
+    public static void CloseWindow(Window window)
+    {
+        window.Close();
+    }
+
+    public static void MaximizeWindow(Window window)
+    {
+        window.WindowState = WindowState.Maximized;
+    }
+
+    public static void MinimizeWindow(Window window)
+    {
+        window.WindowState = WindowState.Minimized;
+    }
+
+    public static void RestoreWindow(Window window)
+    {
+        window.WindowState = WindowState.Normal;
+    }
+}

+ 292 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Main/MainTitleBar.axaml

@@ -2,7 +2,298 @@
              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:xaml="clr-namespace:PixiEditor.Models.Commands.XAML"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:dataHolders="clr-namespace:PixiEditor.Models.DataHolders"
+             xmlns:viewModels="clr-namespace:PixiEditor.Avalonia.ViewModels"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.Avalonia.Views.Main.MainTitleBar">
-
+            <xaml:Menu
+                Margin="10, 0, 0, 0"
+                    DockPanel.Dock="Left"
+                    HorizontalAlignment="Left"
+                    VerticalAlignment="Top"
+                    Background="Transparent">
+                <MenuItem
+                        ui:Translator.Key="FILE"
+                        Focusable="False">
+                        <MenuItem
+                            ui:Translator.Key="NEW_FILE"
+                            xaml:Menu.Command="PixiEditor.File.New" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN"
+                            xaml:Menu.Command="PixiEditor.File.Open" />
+                        <MenuItem
+                            ui:Translator.Key="RECENT"
+                            ItemsSource="{Binding FileSubViewModel.RecentlyOpened}"
+                            x:Name="recentItemMenu"
+                            IsEnabled="{Binding FileSubViewModel.HasRecent}">
+                            <MenuItem.Styles>
+                                <Style Selector="MenuItem">
+                                    <Setter
+                                        Property="Command"
+                                        Value="{xaml:Command PixiEditor.File.OpenRecent, UseProvided=True}" />
+                                    <Setter
+                                        Property="CommandParameter"
+                                        Value="{Binding FilePath}" />
+                                </Style>
+                            </MenuItem.Styles>
+                            <MenuItem.ItemTemplate>
+                                <DataTemplate
+                                    DataType="{x:Type dataHolders:RecentlyOpenedDocument}">
+                                    <Grid>
+                                        <Grid.ColumnDefinitions>
+                                            <ColumnDefinition/>
+                                            <ColumnDefinition Width="Auto"/>
+                                        </Grid.ColumnDefinitions>
+                                        
+                                        <TextBlock Text="{Binding FilePath}"/>
+                                        <TextBlock Grid.Column="1" Margin="20,0,0,0" VerticalAlignment="Center">
+                                            <Button Foreground="#FFF" FontFamily="{StaticResource Feather}"
+                                                       Command="{xaml:Command Name=PixiEditor.File.RemoveRecent, UseProvided=True}"
+                                                       CommandParameter="{Binding FilePath}"></Button>
+                                        </TextBlock>
+                                    </Grid>
+                                </DataTemplate>
+                            </MenuItem.ItemTemplate>
+                        </MenuItem>
+                        <MenuItem
+                            Focusable="False"
+                            ui:Translator.Key="SAVE_PIXI"
+                            xaml:Menu.Command="PixiEditor.File.Save" />
+                        <MenuItem
+                            ui:Translator.Key="SAVE_AS_PIXI"
+                            xaml:Menu.Command="PixiEditor.File.SaveAsNew" />
+                        <MenuItem
+                            ui:Translator.Key="EXPORT_IMG"
+                            xaml:Menu.Command="PixiEditor.File.Export" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="EXIT"
+                            Command="{x:Static viewModels:SystemCommands.CloseWindowCommand}">
+                            <MenuItem.Icon>
+                                <TextBlock Text="&#xE106;" FontFamily="{DynamicResource NativeIconFont}" FontSize="20"/>
+                            </MenuItem.Icon>
+                        </MenuItem>
+                    </MenuItem>
+                    <MenuItem
+                        Focusable="False"
+                        ui:Translator.Key="EDIT">
+                        <MenuItem
+                            ui:Translator.Key="UNDO"
+                            xaml:Menu.Command="PixiEditor.Undo.Undo" />
+                        <MenuItem
+                            ui:Translator.Key="REDO"
+                            xaml:Menu.Command="PixiEditor.Undo.Redo" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="CUT"
+                            xaml:Menu.Command="PixiEditor.Clipboard.Cut" />
+                        <MenuItem
+                            ui:Translator.Key="COPY"
+                            xaml:Menu.Command="PixiEditor.Clipboard.Copy" />
+                        <MenuItem
+                            ui:Translator.Key="PASTE"
+                            xaml:Menu.Command="PixiEditor.Clipboard.Paste" />
+                        <MenuItem
+                            ui:Translator.Key="DUPLICATE"
+                            xaml:Menu.Command="PixiEditor.Layer.DuplicateSelectedLayer" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="DELETE_SELECTED_PIXELS"
+                            xaml:Menu.Command="PixiEditor.Document.DeletePixels" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="SETTINGS"
+                            xaml:Menu.Command="PixiEditor.Window.OpenSettingsWindow" />
+                    </MenuItem>
+                    <MenuItem
+                        Focusable="False"
+                        ui:Translator.Key="SELECT">
+                        <MenuItem
+                            ui:Translator.Key="SELECT_ALL"
+                            xaml:Menu.Command="PixiEditor.Selection.SelectAll" />
+                        <MenuItem
+                            ui:Translator.Key="DESELECT"
+                            xaml:Menu.Command="PixiEditor.Selection.Clear" />
+                        <MenuItem
+                            ui:Translator.Key="INVERT"
+                            xaml:Menu.Command="PixiEditor.Selection.InvertSelection" />
+                        <MenuItem
+                            ui:Translator.Key="CROP_TO_SELECTION"
+                            xaml:Menu.Command="PixiEditor.Selection.CropToSelection" />
+                        <Separator/>
+                        <MenuItem ui:Translator.Key="SELECTION_TO_MASK">
+                            <MenuItem
+                                ui:Translator.Key="TO_NEW_MASK"
+                                xaml:Menu.Command="PixiEditor.Selection.NewToMask" />
+                            <MenuItem
+                                ui:Translator.Key="ADD_TO_MASK"
+                                xaml:Menu.Command="PixiEditor.Selection.AddToMask" />
+                            <MenuItem
+                                ui:Translator.Key="SUBTRACT_FROM_MASK"
+                                xaml:Menu.Command="PixiEditor.Selection.SubtractFromMask" />
+                            <MenuItem
+                                ui:Translator.Key="INTERSECT_WITH_MASK"
+                                xaml:Menu.Command="PixiEditor.Selection.IntersectSelectionMask" />
+                        </MenuItem>
+                    </MenuItem>
+                    <MenuItem
+                        Focusable="False"
+                        ui:Translator.Key="IMAGE">
+                        <MenuItem
+                            ui:Translator.Key="RESIZE_IMAGE"
+                            xaml:Menu.Command="PixiEditor.Document.ResizeDocument" />
+                        <MenuItem
+                            ui:Translator.Key="RESIZE_CANVAS"
+                            xaml:Menu.Command="PixiEditor.Document.ResizeCanvas" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="CLIP_CANVAS"
+                            xaml:Menu.Command="PixiEditor.Document.ClipCanvas" />
+                        <MenuItem
+                            ui:Translator.Key="CENTER_CONTENT"
+                            xaml:Menu.Command="PixiEditor.Document.CenterContent" />
+                        <Separator />
+                        <!--TODO: Create ToggleableMenuItem
+                        <MenuItem
+                            IsCheckable="True"
+                            IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
+                            IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.HorizontalSymmetryAxisEnabledBindable}"
+                            ui:Translator.Key="HORIZONTAL_LINE_SYMMETRY">
+                            <MenuItem.Icon>
+                                <Image Source="../Images/SymmetryHorizontal.png"
+                                       Width="{x:Static xaml:Menu.IconDimensions}" Height="{x:Static xaml:Menu.IconDimensions}"/>
+                            </MenuItem.Icon>
+                        </MenuItem>
+                        <MenuItem
+                            IsCheckable="True"
+                            IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
+                            IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
+                            ui:Translator.Key="VERTICAL_LINE_SYMMETRY">
+                            <MenuItem.Icon>
+                                <Image Source="../Images/SymmetryVertical.png"
+                                       Width="{x:Static xaml:Menu.IconDimensions}" Height="{x:Static xaml:Menu.IconDimensions}"/>
+                            </MenuItem.Icon>
+                        </MenuItem>-->
+                        <Separator/>
+                        <MenuItem ui:Translator.Key="ROTATION">
+                            <MenuItem ui:Translator.Key="ROT_IMG_90_D" xaml:Menu.Command="PixiEditor.Document.Rotate90Deg"/>
+                            <MenuItem ui:Translator.Key="ROT_IMG_180_D" xaml:Menu.Command="PixiEditor.Document.Rotate180Deg"/>
+                            <MenuItem ui:Translator.Key="ROT_IMG_-90_D" xaml:Menu.Command="PixiEditor.Document.Rotate270Deg"/>
+                            
+                            <Separator/>
+                            <MenuItem ui:Translator.Key="ROT_LAYERS_90_D" xaml:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
+                            <MenuItem ui:Translator.Key="ROT_LAYERS_180_D" xaml:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
+                            <MenuItem ui:Translator.Key="ROT_LAYERS_-90_D" xaml:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
+                        </MenuItem>
+                        <MenuItem ui:Translator.Key="FLIP">
+                            <MenuItem ui:Translator.Key="FLIP_IMG_HORIZONTALLY" xaml:Menu.Command="PixiEditor.Document.FlipImageHorizontal"/>
+                            <MenuItem ui:Translator.Key="FLIP_IMG_VERTICALLY" xaml:Menu.Command="PixiEditor.Document.FlipImageVertical"/>
+                            <MenuItem ui:Translator.Key="FLIP_LAYERS_HORIZONTALLY" xaml:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                            <MenuItem ui:Translator.Key="FLIP_LAYERS_VERTICALLY" xaml:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                        </MenuItem>
+                    </MenuItem>
+                    <MenuItem
+                        Focusable="False"
+                        ui:Translator.Key="VIEW">
+                        <MenuItem
+                            ui:Translator.Key="NEW_WINDOW_FOR_IMG"
+                            xaml:Menu.Command="PixiEditor.Window.CreateNewViewport" />
+                        <Separator/>
+                        <MenuItem
+                            ui:Translator.Key="OPEN_STARTUP_WINDOW"
+                            xaml:Menu.Command="PixiEditor.Window.OpenStartupWindow" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_NAVIGATION_WINDOW"
+                            xaml:Menu.Command="PixiEditor.Window.OpenNavigationWindow" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_SHORTCUT_WINDOW"
+                            xaml:Menu.Command="PixiEditor.Window.OpenShortcutWindow" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_PALETTE_BROWSER"
+                            xaml:Menu.Command="PixiEditor.Window.OpenPalettesBrowserWindow" />
+                        <Separator/>
+                        <!--<MenuItem
+                            ui:Translator.Key="TOGGLE_GRIDLINES"
+                            IsChecked="{Binding ViewportSubViewModel.GridLinesEnabled, Mode=TwoWay}"
+                            IsCheckable="True"
+                            InputGestureText="{xaml:ShortcutBinding PixiEditor.View.ToggleGrid}">
+                            <MenuItem.Icon>
+                                <Image Source="../Images/Commands/PixiEditor/View/ToggleGrid.png"
+                                       Width="{x:Static xaml:Menu.IconDimensions}" Height="{x:Static xaml:Menu.IconDimensions}"/>
+                            </MenuItem.Icon>
+                        </MenuItem>-->
+                    </MenuItem>
+                    <MenuItem
+                        Focusable="False"
+                        ui:Translator.Key="HELP">
+                        <MenuItem
+                            ui:Translator.Key="DOCUMENTATION"
+                            xaml:Menu.Command="PixiEditor.Links.OpenDocumentation" />
+                        <MenuItem
+                            ui:Translator.Key="WEBSITE"
+                            xaml:Menu.Command="PixiEditor.Links.OpenWebsite" />
+                        <MenuItem
+                            ui:Translator.Key="REPOSITORY"
+                            xaml:Menu.Command="PixiEditor.Links.OpenRepository" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="LICENSE"
+                            xaml:Menu.Command="PixiEditor.Links.OpenLicense" />
+                        <MenuItem
+                            ui:Translator.Key="THIRD_PARTY_LICENSES"
+                            xaml:Menu.Command="PixiEditor.Links.OpenOtherLicenses" />
+                        <Separator/>
+                        <MenuItem
+                            ui:Translator.Key="ABOUT"
+                            xaml:Menu.Command="PixiEditor.Window.OpenAboutWindow" />
+                    </MenuItem>
+                    <MenuItem
+                        ui:Translator.Key="DEBUG"
+                        IsVisible="{Binding DebugSubViewModel.UseDebug}">
+                        <MenuItem
+                            ui:Translator.Key="OPEN_COMMAND_DEBUG_WINDOW"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenCommandDebugWindow"/>
+                        <MenuItem
+                            ui:Translator.Key="OPEN_LOCALIZATION_DEBUG_WINDOW"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenLocalizationDebugWindow"/>
+                        <Separator/>
+                        <MenuItem
+                            ui:Translator.Key="OPEN_LOCAL_APPDATA_DIR"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenLocalAppDataDirectory" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_ROAMING_APPDATA_DIR"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenRoamingAppDataDirectory" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_TEMP_DIR"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenTempDirectory" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_INSTALLATION_DIR"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenInstallDirectory" />
+                        <MenuItem
+                            ui:Translator.Key="OPEN_CRASH_REPORTS_DIR"
+                            xaml:Menu.Command="PixiEditor.Debug.OpenCrashReportsDirectory" />
+                        <Separator />
+                        <MenuItem
+                            ui:Translator.Key="CRASH"
+                            xaml:Menu.Command="PixiEditor.Debug.Crash" />
+                        <MenuItem
+                            ui:Translator.Key="DELETE">
+                            <MenuItem
+                                ui:Translator.Key="USER_PREFS"
+                                xaml:Menu.Command="PixiEditor.Debug.DeleteUserPreferences" />
+                            <MenuItem
+                                ui:Translator.Key="SHORTCUT_FILE"
+                                xaml:Menu.Command="PixiEditor.Debug.DeleteShortcutFile" />
+                            <MenuItem
+                                ui:Translator.Key="EDITOR_DATA"
+                                xaml:Menu.Command="PixiEditor.Debug.DeleteEditorData" />
+                            <Separator/>
+                            <MenuItem
+                                ui:Translator.Key="CLEAR_RECENT_DOCUMENTS"
+                                xaml:Menu.Command="PixiEditor.Debug.ClearRecentDocument"/>
+                        </MenuItem>
+                    </MenuItem>
+                </xaml:Menu>
 </UserControl>

+ 31 - 2
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/MainWindow.axaml.cs

@@ -1,16 +1,45 @@
 using System.Collections.Generic;
 using Avalonia.Controls;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.DrawingApi.Core.Bridge;
+using PixiEditor.DrawingApi.Skia;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Extensions.UI;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.AppExtensions;
 using PixiEditor.Models.Localization;
+using PixiEditor.Platform;
+using PixiEditor.Views;
 
 namespace PixiEditor.Avalonia.Views;
 
 internal partial class MainWindow : Window
 {
+    private readonly IPreferences preferences;
+    private readonly IPlatform platform;
+    private readonly IServiceProvider services;
+    private static ExtensionLoader extLoader;
+
+    public new MainViewModel DataContext { get => (MainViewModel)base.DataContext; set => base.DataContext = value; }
+
     public MainWindow(ExtensionLoader extensionLoader)
     {
-        LocalizationProvider localizationProvider = new LocalizationProvider(null);
-        localizationProvider.LoadData(/*TODO: IPreferences.Current.GetPreference<string>("LanguageCode");*/);
+        extLoader = extensionLoader;
+
+        services = new ServiceCollection()
+            .AddPlatform()
+            .AddPixiEditor(extensionLoader)
+            .AddExtensionServices()
+            .BuildServiceProvider();
+
+        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
+        DrawingBackendApi.SetupBackend(skiaDrawingBackend);
+
+        preferences = services.GetRequiredService<IPreferences>();
+        platform = services.GetRequiredService<IPlatform>();
+        DataContext = services.GetRequiredService<MainViewModel>();
+        DataContext.Setup(services);
 
         InitializeComponent();
     }

+ 2 - 2
src/PixiEditor.Extensions/UI/Translator.cs

@@ -178,9 +178,9 @@ public class Translator : Control
         {
             contentControl.Bind(ContentControl.ContentProperty, valueObservable);
         }
-        else if (d is HeaderedItemsControl menuItem)
+        else if (d is HeaderedSelectingItemsControl menuItem)
         {
-            menuItem.Bind(HeaderedItemsControl.HeaderProperty, valueObservable);
+            menuItem.Bind(HeaderedSelectingItemsControl.HeaderProperty, valueObservable);
         }
 #if DEBUG
         else