Browse Source

Merge pull request #928 from PixiEditor/api/axes-panel

Added axes
Krzysztof Krysiński 3 months ago
parent
commit
985588ec3b
32 changed files with 601 additions and 117 deletions
  1. 1 1
      samples/Sample7_FlyUI/Sample7_FlyUI.csproj
  2. 37 29
      samples/Sample7_FlyUI/WindowContentElement.cs
  3. 6 0
      src/PixiEditor.Extensions.CommonApi/IO/IDocumentProvider.cs
  4. 18 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/AxisAlignment.cs
  5. 18 4
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs
  6. 18 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs
  7. 18 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs
  8. 12 0
      src/PixiEditor.Extensions.Sdk/Api/IO/DocumentProvider.cs
  9. 1 1
      src/PixiEditor.Extensions.Sdk/Api/Window/WindowProvider.cs
  10. 9 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.Document.cs
  11. 3 0
      src/PixiEditor.Extensions.Sdk/PixiEditorApi.cs
  12. 22 0
      src/PixiEditor.Extensions.WasmRuntime/Api/DocumentsApi.cs
  13. 4 13
      src/PixiEditor.Extensions.WasmRuntime/Api/ResourcesApi.cs
  14. 5 2
      src/PixiEditor.Extensions.WasmRuntime/Api/WindowingApi.cs
  15. 21 0
      src/PixiEditor.Extensions.WasmRuntime/Utilities/ResourcesUtility.cs
  16. 11 0
      src/PixiEditor.Extensions/Common/Localization/LocalizedString.cs
  17. 2 0
      src/PixiEditor.Extensions/ExtensionServices.cs
  18. 4 1
      src/PixiEditor.Extensions/FlyUI/Converters/PathToBitmapConverter.cs
  19. 49 31
      src/PixiEditor.Extensions/FlyUI/Elements/Border.cs
  20. 40 10
      src/PixiEditor.Extensions/FlyUI/Elements/Column.cs
  21. 18 0
      src/PixiEditor.Extensions/FlyUI/Elements/MainAxisAlignment.cs
  22. 43 7
      src/PixiEditor.Extensions/FlyUI/Elements/Row.cs
  23. 11 3
      src/PixiEditor.Extensions/Metadata/ExtensionPermissions.cs
  24. 96 0
      src/PixiEditor.Extensions/UI/Panels/ColumnPanel.cs
  25. 96 0
      src/PixiEditor.Extensions/UI/Panels/RowPanel.cs
  26. 2 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  27. 1 1
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  28. 2 2
      src/PixiEditor/Models/ExtensionServices/CommandProvider.cs
  29. 21 0
      src/PixiEditor/Models/ExtensionServices/DocumentProvider.cs
  30. 1 1
      src/PixiEditor/Models/ExtensionServices/WindowProvider.cs
  31. 5 4
      tests/PixiEditor.Extensions.Tests/LayoutBuilderElementsTests.cs
  32. 6 5
      tests/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs

+ 1 - 1
samples/Sample7_FlyUI/Sample7_FlyUI.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>FlyUISample</RootNamespace>
         <RootNamespace>FlyUISample</RootNamespace>

+ 37 - 29
samples/Sample7_FlyUI/WindowContentElement.cs

@@ -1,47 +1,55 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using System.Diagnostics.CodeAnalysis;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Sdk;
 using PixiEditor.Extensions.Sdk;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
 using PixiEditor.Extensions.Sdk.Api.Window;
 using PixiEditor.Extensions.Sdk.Api.Window;
 
 
 namespace FlyUISample;
 namespace FlyUISample;
 
 
+[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "FlyUI style")]
 public class WindowContentElement : StatelessElement
 public class WindowContentElement : StatelessElement
 {
 {
     public PopupWindow Window { get; set; }
     public PopupWindow Window { get; set; }
+
     public override CompiledControl BuildNative()
     public override CompiledControl BuildNative()
     {
     {
         Layout layout = new Layout(body:
         Layout layout = new Layout(body:
             new Container(margin: Edges.All(25), child:
             new Container(margin: Edges.All(25), child:
                 new Column(
                 new Column(
-                    new Center(
-                        new Text(
-                            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae neque nibh. Duis sed pharetra dolor. Donec dui sapien, aliquam id sodales in, ornare et urna. Mauris nunc odio, sagittis eget lectus at, imperdiet ornare quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod pellentesque blandit. Vestibulum sagittis, ligula non finibus lobortis, dolor lacus consectetur turpis, id facilisis ligula dolor vitae augue.",
-                            wrap: TextWrap.Wrap,
-                            fontSize: 16)
-                    ),
-                    new Align(
-                        alignment: Alignment.CenterRight,
-                        child: new Text("- Paulo Coelho, The Alchemist (1233)", fontStyle: FontStyle.Italic)
-                    ),
-                    new Container(
-                        margin: Edges.Symmetric(25, 0),
-                        backgroundColor: Color.FromRgba(25, 25, 25, 255),
-                        child: new Column(
-                            new Image(
-                                "/Pizza.png",
-                                filterQuality: FilterQuality.None,
-                                width: 256, height: 256))
-                    ),
-                    new CheckBox(new Text("heloo"), onCheckedChanged: args =>
-                    {
-                        PixiEditorExtension.Api.Logger.Log(((CheckBox)args.Sender).IsChecked ? "Checked" : "Unchecked");
-                    }),
-                    new Center(
-                        new Button(
-                            child: new Text("Close"), onClick: _ =>
+                    crossAxisAlignment: CrossAxisAlignment.Center,
+                    mainAxisAlignment: MainAxisAlignment.SpaceEvenly,
+                    children:
+                    [
+                        new Center(
+                            new Text(
+                                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae neque nibh. Duis sed pharetra dolor. Donec dui sapien, aliquam id sodales in, ornare et urna. Mauris nunc odio, sagittis eget lectus at, imperdiet ornare quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod pellentesque blandit. Vestibulum sagittis, ligula non finibus lobortis, dolor lacus consectetur turpis, id facilisis ligula dolor vitae augue.",
+                                wrap: TextWrap.Wrap,
+                                fontSize: 16)
+                        ),
+                        new Align(
+                            alignment: Alignment.CenterRight,
+                            child: new Text("- Paulo Coelho, The Alchemist (1233)", fontStyle: FontStyle.Italic)
+                        ),
+                        new Container(
+                            margin: Edges.Symmetric(25, 0),
+                            backgroundColor: Color.FromRgba(25, 25, 25, 255),
+                            child: new Column(
+                                new Image(
+                                    "/Pizza.png",
+                                    filterQuality: FilterQuality.None,
+                                    width: 256, height: 256))
+                        ),
+                        new CheckBox(new Text("heloo"),
+                            onCheckedChanged: args =>
                             {
                             {
-                                Window.Close();
-                            }))
+                                PixiEditorExtension.Api.Logger.Log(((CheckBox)args.Sender).IsChecked
+                                    ? "Checked"
+                                    : "Unchecked");
+                            }),
+                        new Center(
+                            new Button(
+                                child: new Text("Close"), onClick: _ => { Window.Close(); }))
+                    ]
                 )
                 )
             )
             )
         );
         );

+ 6 - 0
src/PixiEditor.Extensions.CommonApi/IO/IDocumentProvider.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Extensions.CommonApi.IO;
+
+public interface IDocumentProvider
+{
+   public void ImportFile(string path, bool associatePath = true);
+}

+ 18 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/AxisAlignment.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+public enum MainAxisAlignment
+{
+    Start,
+    Center,
+    End,
+    SpaceBetween,
+    SpaceAround,
+    SpaceEvenly
+}
+
+public enum CrossAxisAlignment
+{
+    Start,
+    Center,
+    End
+}

+ 18 - 4
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs

@@ -6,14 +6,22 @@ public class Border : SingleChildLayoutElement
 {
 {
     public Color Color { get; set; }
     public Color Color { get; set; }
     public Edges Thickness { get; set; }
     public Edges Thickness { get; set; }
-    
+
     public Edges CornerRadius { get; set; }
     public Edges CornerRadius { get; set; }
-    
+
     public Edges Padding { get; set; }
     public Edges Padding { get; set; }
-    
+
     public Edges Margin { get; set; }
     public Edges Margin { get; set; }
 
 
-    public Border(LayoutElement child = null, Color color = default, Edges thickness = default, Edges cornerRadius = default, Edges padding = default, Edges margin = default)
+    public Color BackgroundColor { get; set; }
+
+    public double Width { get; set; }
+
+    public double Height { get; set; }
+
+    public Border(LayoutElement child = null, Color color = default, Edges thickness = default,
+        Edges cornerRadius = default, Edges padding = default, Edges margin = default, double width = -1, double height = -1,
+        Color backgroundColor = default)
     {
     {
         Child = child;
         Child = child;
         Color = color;
         Color = color;
@@ -21,6 +29,9 @@ public class Border : SingleChildLayoutElement
         CornerRadius = cornerRadius;
         CornerRadius = cornerRadius;
         Padding = padding;
         Padding = padding;
         Margin = margin;
         Margin = margin;
+        Width = width;
+        Height = height;
+        BackgroundColor = backgroundColor;
     }
     }
 
 
     public override CompiledControl BuildNative()
     public override CompiledControl BuildNative()
@@ -33,6 +44,9 @@ public class Border : SingleChildLayoutElement
         control.AddProperty(CornerRadius);
         control.AddProperty(CornerRadius);
         control.AddProperty(Padding);
         control.AddProperty(Padding);
         control.AddProperty(Margin);
         control.AddProperty(Margin);
+        control.AddProperty(Width);
+        control.AddProperty(Height);
+        control.AddProperty(BackgroundColor);
 
 
         return control;
         return control;
     }
     }

+ 18 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs

@@ -2,14 +2,31 @@
 
 
 public class Column : MultiChildLayoutElement
 public class Column : MultiChildLayoutElement
 {
 {
+    public MainAxisAlignment MainAxisAlignment { get; set; }
+    public CrossAxisAlignment CrossAxisAlignment { get; set; }
+
+    public Column(
+        MainAxisAlignment mainAxisAlignment = MainAxisAlignment.Start,
+        CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.Start,
+        LayoutElement[] children = null)
+    {
+        MainAxisAlignment = mainAxisAlignment;
+        CrossAxisAlignment = crossAxisAlignment;
+        Children = new List<LayoutElement>(children);
+    }
+
     public Column(params LayoutElement[] children)
     public Column(params LayoutElement[] children)
     {
     {
+        MainAxisAlignment = MainAxisAlignment.Start;
+        CrossAxisAlignment = CrossAxisAlignment.Start;
         Children = new List<LayoutElement>(children);
         Children = new List<LayoutElement>(children);
     }
     }
-    
+
     public override CompiledControl BuildNative()
     public override CompiledControl BuildNative()
     {
     {
         CompiledControl control = new CompiledControl(UniqueId, "Column");
         CompiledControl control = new CompiledControl(UniqueId, "Column");
+        control.AddProperty(MainAxisAlignment);
+        control.AddProperty(CrossAxisAlignment);
         control.Children.AddRange(Children.Where(x => x != null).Select(x => x.BuildNative()));
         control.Children.AddRange(Children.Where(x => x != null).Select(x => x.BuildNative()));
 
 
         return control;
         return control;

+ 18 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs

@@ -2,14 +2,31 @@
 
 
 public class Row : MultiChildLayoutElement
 public class Row : MultiChildLayoutElement
 {
 {
+    public MainAxisAlignment MainAxisAlignment { get; set; }
+    public CrossAxisAlignment CrossAxisAlignment { get; set; }
+
     public Row(params LayoutElement[] children)
     public Row(params LayoutElement[] children)
     {
     {
         Children = new List<LayoutElement>(children);
         Children = new List<LayoutElement>(children);
+        MainAxisAlignment = MainAxisAlignment.Start;
+        CrossAxisAlignment = CrossAxisAlignment.Start;
+    }
+
+    public Row(
+        MainAxisAlignment mainAxisAlignment = MainAxisAlignment.Start,
+        CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.Start,
+        LayoutElement[] children = null)
+    {
+        MainAxisAlignment = mainAxisAlignment;
+        CrossAxisAlignment = crossAxisAlignment;
+        Children = new List<LayoutElement>(children);
     }
     }
-    
+
     public override CompiledControl BuildNative()
     public override CompiledControl BuildNative()
     {
     {
         CompiledControl control = new CompiledControl(UniqueId, "Row");
         CompiledControl control = new CompiledControl(UniqueId, "Row");
+        control.AddProperty(MainAxisAlignment);
+        control.AddProperty(CrossAxisAlignment);
         control.Children.AddRange(Children.Select(x => x.BuildNative()));
         control.Children.AddRange(Children.Select(x => x.BuildNative()));
 
 
         return control;
         return control;

+ 12 - 0
src/PixiEditor.Extensions.Sdk/Api/IO/DocumentProvider.cs

@@ -0,0 +1,12 @@
+using PixiEditor.Extensions.CommonApi.IO;
+using PixiEditor.Extensions.Sdk.Bridge;
+
+namespace PixiEditor.Extensions.Sdk.Api.IO;
+
+public class DocumentProvider : IDocumentProvider
+{
+    public void ImportFile(string path, bool associatePath = true)
+    {
+        Native.import_file(path, associatePath);
+    }
+}

+ 1 - 1
src/PixiEditor.Extensions.Sdk/Api/Window/WindowProvider.cs

@@ -17,7 +17,7 @@ public class WindowProvider : IWindowProvider
         Marshal.FreeHGlobal(ptr);
         Marshal.FreeHGlobal(ptr);
         
         
         SubscribeToEvents(compiledControl);
         SubscribeToEvents(compiledControl);
-        return new PopupWindow(handle) { Title = title };
+        return new PopupWindow(handle);
     }
     }
 
 
     internal void LayoutStateChanged(int uniqueId, CompiledControl newLayout)
     internal void LayoutStateChanged(int uniqueId, CompiledControl newLayout)

+ 9 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Native.Document.cs

@@ -0,0 +1,9 @@
+using System.Runtime.CompilerServices;
+
+namespace PixiEditor.Extensions.Sdk.Bridge;
+
+internal partial class Native
+{
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    internal static extern void import_file(string path, bool associatePath);
+}

+ 3 - 0
src/PixiEditor.Extensions.Sdk/PixiEditorApi.cs

@@ -1,6 +1,7 @@
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.Sdk.Api;
 using PixiEditor.Extensions.Sdk.Api;
 using PixiEditor.Extensions.Sdk.Api.Commands;
 using PixiEditor.Extensions.Sdk.Api.Commands;
+using PixiEditor.Extensions.Sdk.Api.IO;
 using PixiEditor.Extensions.Sdk.Api.Logging;
 using PixiEditor.Extensions.Sdk.Api.Logging;
 using PixiEditor.Extensions.Sdk.Api.Palettes;
 using PixiEditor.Extensions.Sdk.Api.Palettes;
 using PixiEditor.Extensions.Sdk.Api.UserPreferences;
 using PixiEditor.Extensions.Sdk.Api.UserPreferences;
@@ -15,6 +16,7 @@ public class PixiEditorApi
     public Preferences Preferences { get; }
     public Preferences Preferences { get; }
     public PalettesProvider Palettes { get; }
     public PalettesProvider Palettes { get; }
     public CommandProvider Commands { get; }
     public CommandProvider Commands { get; }
+    public DocumentProvider Documents { get; }
 
 
     public PixiEditorApi()
     public PixiEditorApi()
     {
     {
@@ -23,5 +25,6 @@ public class PixiEditorApi
         Preferences = new Preferences();
         Preferences = new Preferences();
         Palettes = new PalettesProvider();
         Palettes = new PalettesProvider();
         Commands = new CommandProvider();
         Commands = new CommandProvider();
+        Documents = new DocumentProvider();
     }
     }
 }
 }

+ 22 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/DocumentsApi.cs

@@ -0,0 +1,22 @@
+using PixiEditor.Extensions.Metadata;
+using PixiEditor.Extensions.WasmRuntime.Utilities;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api;
+
+internal class DocumentsApi : ApiGroupHandler
+{
+    [ApiFunction("import_file")]
+    public void ImportFile(string path, bool associatePath = false)
+    {
+        PermissionUtility.ThrowIfLacksPermissions(Extension.Metadata, ExtensionPermissions.OpenDocuments, "ImportFile");
+
+        string fullPath = ResourcesUtility.ToResourcesFullPath(Extension, path);
+
+        if (!File.Exists(fullPath))
+        {
+            return;
+        }
+
+        Api.Documents.ImportFile(fullPath, associatePath);
+    }
+}

+ 4 - 13
src/PixiEditor.Extensions.WasmRuntime/Api/ResourcesApi.cs

@@ -1,22 +1,13 @@
-namespace PixiEditor.Extensions.WasmRuntime.Api;
+using PixiEditor.Extensions.WasmRuntime.Utilities;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api;
 
 
 internal class ResourcesApi : ApiGroupHandler
 internal class ResourcesApi : ApiGroupHandler
 {
 {
     [ApiFunction("to_resources_full_path")]
     [ApiFunction("to_resources_full_path")]
     public string ToResourcesFullPath(string path)
     public string ToResourcesFullPath(string path)
     {
     {
-        string resourcesPath = Path.Combine(Path.GetDirectoryName(Extension.Location), "Resources");
-        string fullPath = path;
-
-        if (path.StartsWith("/") || path.StartsWith("/Resources/"))
-        {
-            fullPath = Path.Combine(resourcesPath, path[1..]);
-        }
-        else if (path.StartsWith("Resources/"))
-        {
-            fullPath = Path.Combine(resourcesPath, path[10..]);
-        }
-
+        string fullPath = ResourcesUtility.ToResourcesFullPath(Extension, path);
         return fullPath;
         return fullPath;
     }
     }
 }
 }

+ 5 - 2
src/PixiEditor.Extensions.WasmRuntime/Api/WindowingApi.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.Async;
 using PixiEditor.Extensions.CommonApi.Async;
+using PixiEditor.Extensions.CommonApi.Utilities;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Extensions.WasmRuntime.Utilities;
 using PixiEditor.Extensions.WasmRuntime.Utilities;
@@ -13,7 +14,8 @@ internal class WindowingApi : ApiGroupHandler
     public int CreatePopupWindow(string title, Span<byte> bodySpan)
     public int CreatePopupWindow(string title, Span<byte> bodySpan)
     {
     {
         var body = LayoutBuilder.Deserialize(bodySpan, DuplicateResolutionTactic.ThrowException);
         var body = LayoutBuilder.Deserialize(bodySpan, DuplicateResolutionTactic.ThrowException);
-        var popupWindow = Api.Windowing.CreatePopupWindow(title, body.BuildNative());
+        string localizedTitleKey = LocalizedString.FirstValidKey($"{Extension.Metadata.UniqueName}:{title}", title);
+        var popupWindow = Api.Windowing.CreatePopupWindow(localizedTitleKey, body.BuildNative());
 
 
         int handle = NativeObjectManager.AddObject(popupWindow);
         int handle = NativeObjectManager.AddObject(popupWindow);
         return handle;
         return handle;
@@ -37,8 +39,9 @@ internal class WindowingApi : ApiGroupHandler
     [ApiFunction("set_window_title")]
     [ApiFunction("set_window_title")]
     public void SetWindowTitle(int handle, string title)
     public void SetWindowTitle(int handle, string title)
     {
     {
+        string localizedTitleKey = LocalizedString.FirstValidKey($"{Extension.Metadata.UniqueName}:{title}", title);
         var window = NativeObjectManager.GetObject<PopupWindow>(handle);
         var window = NativeObjectManager.GetObject<PopupWindow>(handle);
-        window.Title = title;
+        window.Title = localizedTitleKey;
     }
     }
 
 
     [ApiFunction("get_window_title")]
     [ApiFunction("get_window_title")]

+ 21 - 0
src/PixiEditor.Extensions.WasmRuntime/Utilities/ResourcesUtility.cs

@@ -0,0 +1,21 @@
+namespace PixiEditor.Extensions.WasmRuntime.Utilities;
+
+public static class ResourcesUtility
+{
+    public static string ToResourcesFullPath(Extension extension, string path)
+    {
+        string resourcesPath = Path.Combine(Path.GetDirectoryName(extension.Location), "Resources");
+        string fullPath = path;
+
+        if (path.StartsWith("/") || path.StartsWith("/Resources/"))
+        {
+            fullPath = Path.Combine(resourcesPath, path[1..]);
+        }
+        else if (path.StartsWith("Resources/"))
+        {
+            fullPath = Path.Combine(resourcesPath, path[10..]);
+        }
+
+        return fullPath;
+    }
+}

+ 11 - 0
src/PixiEditor.Extensions/Common/Localization/LocalizedString.cs

@@ -107,4 +107,15 @@ public struct LocalizedString
 
 
     public static implicit operator LocalizedString(string key) => new(key);
     public static implicit operator LocalizedString(string key) => new(key);
     public static implicit operator string(LocalizedString localizedString) => localizedString.Value;
     public static implicit operator string(LocalizedString localizedString) => localizedString.Value;
+
+    public static string FirstValidKey(string key, string fallbackKey)
+    {
+        LocalizedString localizedString = new(key);
+        if (localizedString.Key == localizedString.Value)
+        {
+            return fallbackKey;
+        }
+
+        return key;
+    }
 }
 }

+ 2 - 0
src/PixiEditor.Extensions/ExtensionServices.cs

@@ -1,4 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.CommonApi.IO;
 using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
@@ -16,6 +17,7 @@ public class ExtensionServices
     public ICommandProvider? Commands => Services.GetService<ICommandProvider>();
     public ICommandProvider? Commands => Services.GetService<ICommandProvider>();
     
     
     public IPalettesProvider? Palettes => Services.GetService<IPalettesProvider>();
     public IPalettesProvider? Palettes => Services.GetService<IPalettesProvider>();
+    public IDocumentProvider Documents => Services.GetService<IDocumentProvider>();
 
 
     public ExtensionServices(IServiceProvider services)
     public ExtensionServices(IServiceProvider services)
     {
     {

+ 4 - 1
src/PixiEditor.Extensions/FlyUI/Converters/PathToBitmapConverter.cs

@@ -10,7 +10,10 @@ public class PathToBitmapConverter : IValueConverter
     {
     {
         if (value is string path)
         if (value is string path)
         {
         {
-            return new Bitmap(path);
+            if (File.Exists(path))
+            {
+                return new Bitmap(path);
+            }
         }
         }
         return null;
         return null;
     }
     }

+ 49 - 31
src/PixiEditor.Extensions/FlyUI/Elements/Border.cs

@@ -12,71 +12,80 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 public class Border : SingleChildLayoutElement, IPropertyDeserializable
 public class Border : SingleChildLayoutElement, IPropertyDeserializable
 {
 {
     private Avalonia.Controls.Border border;
     private Avalonia.Controls.Border border;
-    
-    private Edges _thickness;
-    private Color _color;
+
+    private Edges thickness;
+    private Color color;
     private Edges cornerRadius;
     private Edges cornerRadius;
     private Edges padding;
     private Edges padding;
     private Edges margin;
     private Edges margin;
-    
-    public Color Color { get => _color; set => SetField(ref _color, value); }
-    public Edges Thickness { get => _thickness; set => SetField(ref _thickness, value); }
+
+    private Color backgroundColor;
+    private double width = double.NaN;
+    private double height = double.NaN;
+
+    public Color Color { get => color; set => SetField(ref color, value); }
+    public Edges Thickness { get => thickness; set => SetField(ref thickness, value); }
     public Edges CornerRadius { get => cornerRadius; set => SetField(ref cornerRadius, value); }
     public Edges CornerRadius { get => cornerRadius; set => SetField(ref cornerRadius, value); }
     public Edges Padding { get => padding; set => SetField(ref padding, value); }
     public Edges Padding { get => padding; set => SetField(ref padding, value); }
     public Edges Margin { get => margin; set => SetField(ref margin, value); }
     public Edges Margin { get => margin; set => SetField(ref margin, value); }
-    
+    public Color BackgroundColor { get => backgroundColor; set => SetField(ref backgroundColor, value); }
+    public double Width { get => width; set => SetField(ref width, value); }
+    public double Height { get => height; set => SetField(ref height, value); }
+
     public override Control BuildNative()
     public override Control BuildNative()
     {
     {
         border = new Avalonia.Controls.Border();
         border = new Avalonia.Controls.Border();
-        
+
         border.ClipToBounds = true;
         border.ClipToBounds = true;
-        
+
         if (Child != null)
         if (Child != null)
         {
         {
             border.Child = Child.BuildNative();
             border.Child = Child.BuildNative();
         }
         }
-        
+
         Binding colorBinding = new Binding()
         Binding colorBinding = new Binding()
         {
         {
-            Source = this,
-            Path = nameof(Color),
-            Converter = new ColorToAvaloniaBrushConverter()
+            Source = this, Path = nameof(Color), Converter = new ColorToAvaloniaBrushConverter()
         };
         };
-        
+
         Binding edgesBinding = new Binding()
         Binding edgesBinding = new Binding()
         {
         {
-            Source = this,
-            Path = nameof(Thickness),
-            Converter = new EdgesToThicknessConverter()
+            Source = this, Path = nameof(Thickness), Converter = new EdgesToThicknessConverter()
         };
         };
-        
+
         Binding cornerRadiusBinding = new Binding()
         Binding cornerRadiusBinding = new Binding()
         {
         {
-            Source = this,
-            Path = nameof(CornerRadius),
-            Converter = new EdgesToCornerRadiusConverter()
+            Source = this, Path = nameof(CornerRadius), Converter = new EdgesToCornerRadiusConverter()
         };
         };
-        
+
         Binding paddingBinding = new Binding()
         Binding paddingBinding = new Binding()
         {
         {
-            Source = this,
-            Path = nameof(Padding),
-            Converter = new EdgesToThicknessConverter()
+            Source = this, Path = nameof(Padding), Converter = new EdgesToThicknessConverter()
         };
         };
-        
+
         Binding marginBinding = new Binding()
         Binding marginBinding = new Binding()
         {
         {
-            Source = this,
-            Path = nameof(Margin),
-            Converter = new EdgesToThicknessConverter()
+            Source = this, Path = nameof(Margin), Converter = new EdgesToThicknessConverter()
         };
         };
-        
+
+        Binding backgroundColorBinding = new Binding()
+        {
+            Source = this, Path = nameof(BackgroundColor), Converter = new ColorToAvaloniaBrushConverter()
+        };
+
+        Binding widthBinding = new Binding() { Source = this, Path = nameof(Width), };
+
+        Binding heightBinding = new Binding() { Source = this, Path = nameof(Height), };
+
+        border.Bind(Layoutable.WidthProperty, widthBinding);
+        border.Bind(Layoutable.HeightProperty, heightBinding);
+        border.Bind(Avalonia.Controls.Border.BackgroundProperty, backgroundColorBinding);
         border.Bind(Avalonia.Controls.Border.BorderBrushProperty, colorBinding);
         border.Bind(Avalonia.Controls.Border.BorderBrushProperty, colorBinding);
         border.Bind(Avalonia.Controls.Border.BorderThicknessProperty, edgesBinding);
         border.Bind(Avalonia.Controls.Border.BorderThicknessProperty, edgesBinding);
         border.Bind(Avalonia.Controls.Border.CornerRadiusProperty, cornerRadiusBinding);
         border.Bind(Avalonia.Controls.Border.CornerRadiusProperty, cornerRadiusBinding);
         border.Bind(Decorator.PaddingProperty, paddingBinding);
         border.Bind(Decorator.PaddingProperty, paddingBinding);
         border.Bind(Layoutable.MarginProperty, marginBinding);
         border.Bind(Layoutable.MarginProperty, marginBinding);
-        
+
         return border;
         return border;
     }
     }
 
 
@@ -97,6 +106,9 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         yield return CornerRadius;
         yield return CornerRadius;
         yield return Padding;
         yield return Padding;
         yield return Margin;
         yield return Margin;
+        yield return BackgroundColor;
+        yield return Width;
+        yield return Height;
     }
     }
 
 
     public void DeserializeProperties(ImmutableList<object> values)
     public void DeserializeProperties(ImmutableList<object> values)
@@ -106,5 +118,11 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         CornerRadius = (Edges)values.ElementAtOrDefault(2, default(Edges));
         CornerRadius = (Edges)values.ElementAtOrDefault(2, default(Edges));
         Padding = (Edges)values.ElementAtOrDefault(3, default(Edges));
         Padding = (Edges)values.ElementAtOrDefault(3, default(Edges));
         Margin = (Edges)values.ElementAtOrDefault(4, default(Edges));
         Margin = (Edges)values.ElementAtOrDefault(4, default(Edges));
+        Width = (double)values.ElementAtOrDefault(5, double.NaN);
+        Height = (double)values.ElementAtOrDefault(6, double.NaN);
+        BackgroundColor = (Color)values.ElementAtOrDefault(7, default(Color));
+
+        Width = Width < 0 ? double.NaN : Width;
+        Height = Height < 0 ? double.NaN : Height;
     }
     }
 }
 }

+ 40 - 10
src/PixiEditor.Extensions/FlyUI/Elements/Column.cs

@@ -1,14 +1,31 @@
-using System.Collections.ObjectModel;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.Collections.Specialized;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Layout;
 using Avalonia.Layout;
 using Avalonia.Threading;
 using Avalonia.Threading;
+using PixiEditor.Extensions.UI.Panels;
 
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
 
-public class Column : MultiChildLayoutElement
+public class Column : MultiChildLayoutElement, IPropertyDeserializable
 {
 {
-    private StackPanel panel;
+    private MainAxisAlignment mainAxisAlignment;
+    private CrossAxisAlignment crossAxisAlignment;
+
+    private Panel panel;
+
+    public MainAxisAlignment MainAxisAlignment
+    {
+        get => mainAxisAlignment;
+        set => SetField(ref mainAxisAlignment, value);
+    }
+
+    public CrossAxisAlignment CrossAxisAlignment
+    {
+        get => crossAxisAlignment;
+        set => SetField(ref crossAxisAlignment, value);
+    }
 
 
     public Column()
     public Column()
     {
     {
@@ -44,15 +61,28 @@ public class Column : MultiChildLayoutElement
 
 
     public override Control BuildNative()
     public override Control BuildNative()
     {
     {
-        panel = new StackPanel()
-        {
-            Orientation = Orientation.Vertical,
-            HorizontalAlignment = HorizontalAlignment.Stretch,
-            VerticalAlignment = VerticalAlignment.Stretch
-        };
-        
+        panel = new ColumnPanel() { MainAxisAlignment = MainAxisAlignment, CrossAxisAlignment = CrossAxisAlignment };
+
         panel.Children.AddRange(Children.Select(x => x.BuildNative()));
         panel.Children.AddRange(Children.Select(x => x.BuildNative()));
 
 
         return panel;
         return panel;
     }
     }
+
+    public IEnumerable<object> GetProperties()
+    {
+        yield return MainAxisAlignment;
+        yield return CrossAxisAlignment;
+    }
+
+    public void DeserializeProperties(ImmutableList<object> values)
+    {
+        if (values.Count < 2)
+            return;
+
+        int mainAxisAlignment = (int)values[0];
+        int crossAxisAlignment = (int)values[1];
+
+        MainAxisAlignment = (MainAxisAlignment)mainAxisAlignment;
+        CrossAxisAlignment = (CrossAxisAlignment)crossAxisAlignment;
+    }
 }
 }

+ 18 - 0
src/PixiEditor.Extensions/FlyUI/Elements/MainAxisAlignment.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.Extensions.FlyUI.Elements;
+
+public enum MainAxisAlignment
+{
+    Start,
+    Center,
+    End,
+    SpaceBetween,
+    SpaceAround,
+    SpaceEvenly
+}
+
+public enum CrossAxisAlignment
+{
+    Start,
+    Center,
+    End
+}

+ 43 - 7
src/PixiEditor.Extensions/FlyUI/Elements/Row.cs

@@ -1,13 +1,32 @@
-using System.Collections.Specialized;
+using System.Collections.Immutable;
+using System.Collections.Specialized;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Layout;
 using Avalonia.Layout;
 using Avalonia.Threading;
 using Avalonia.Threading;
+using PixiEditor.Extensions.UI.Panels;
 
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
 
-public class Row : MultiChildLayoutElement
+public class Row : MultiChildLayoutElement, IPropertyDeserializable
 {
 {
-    private StackPanel panel;
+    private MainAxisAlignment mainAxisAlignment;
+    private CrossAxisAlignment crossAxisAlignment;
+
+    private Panel panel;
+
+    public MainAxisAlignment MainAxisAlignment
+    {
+        get => mainAxisAlignment;
+        set => SetField(ref mainAxisAlignment, value);
+    }
+
+    public CrossAxisAlignment CrossAxisAlignment
+    {
+        get => crossAxisAlignment;
+        set => SetField(ref crossAxisAlignment, value);
+    }
+
+
     public Row()
     public Row()
     {
     {
     }
     }
@@ -43,15 +62,32 @@ public class Row : MultiChildLayoutElement
 
 
     public override Control BuildNative()
     public override Control BuildNative()
     {
     {
-        panel = new StackPanel()
+        panel = new RowPanel()
         {
         {
-            Orientation = Orientation.Horizontal,
-            HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch,
-            VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch
+            MainAxisAlignment = MainAxisAlignment,
+            CrossAxisAlignment = CrossAxisAlignment
         };
         };
 
 
         panel.Children.AddRange(Children.Select(x => x.BuildNative()));
         panel.Children.AddRange(Children.Select(x => x.BuildNative()));
 
 
         return panel;
         return panel;
     }
     }
+
+    public IEnumerable<object> GetProperties()
+    {
+        yield return MainAxisAlignment;
+        yield return CrossAxisAlignment;
+    }
+
+    public void DeserializeProperties(ImmutableList<object> values)
+    {
+        if (values.Count < 2)
+            return;
+
+        int mainAxisAlignment = (int)values[0];
+        int crossAxisAlignment = (int)values[1];
+
+        MainAxisAlignment = (MainAxisAlignment)mainAxisAlignment;
+        CrossAxisAlignment = (CrossAxisAlignment)crossAxisAlignment;
+    }
 }
 }

+ 11 - 3
src/PixiEditor.Extensions/Metadata/ExtensionPermissions.cs

@@ -1,15 +1,23 @@
-namespace PixiEditor.Extensions.Metadata;
+using System.Runtime.CompilerServices;
+
+namespace PixiEditor.Extensions.Metadata;
 
 
 [Flags]
 [Flags]
 [Newtonsoft.Json.JsonConverter(typeof(JsonEnumFlagConverter))]
 [Newtonsoft.Json.JsonConverter(typeof(JsonEnumFlagConverter))]
 public enum ExtensionPermissions
 public enum ExtensionPermissions
 {
 {
     None = 0,
     None = 0,
-    
+
     /// <summary>
     /// <summary>
     ///     Allows extension to write to preferences that are not owned by the extension. Owned preferences are those that are
     ///     Allows extension to write to preferences that are not owned by the extension. Owned preferences are those that are
     ///    created by the extension itself (they are prefixed with the extension unique name, ex. PixiEditor.SomeExt:PopupShown).
     ///    created by the extension itself (they are prefixed with the extension unique name, ex. PixiEditor.SomeExt:PopupShown).
     /// </summary>
     /// </summary>
     WriteNonOwnedPreferences = 1,
     WriteNonOwnedPreferences = 1,
-    FullAccess = ~0
+
+    /// <summary>
+    ///     Allows extension to open documents. This permission is required for extensions that need to import files into
+    ///     the editor.
+    /// </summary>
+    OpenDocuments = 2,
+    FullAccess = ~0,
 }
 }

+ 96 - 0
src/PixiEditor.Extensions/UI/Panels/ColumnPanel.cs

@@ -0,0 +1,96 @@
+using Avalonia;
+using Avalonia.Controls;
+using PixiEditor.Extensions.FlyUI.Elements;
+
+namespace PixiEditor.Extensions.UI.Panels;
+
+public class ColumnPanel : Panel
+{
+    public MainAxisAlignment MainAxisAlignment { get; set; } = MainAxisAlignment.Start;
+    public CrossAxisAlignment CrossAxisAlignment { get; set; } = CrossAxisAlignment.Start;
+
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        Size size = new(0, 0);
+        foreach (var child in Children)
+        {
+            child.Measure(availableSize);
+            size += new Size(0, child.DesiredSize.Height);
+            size = new Size(Math.Max(size.Width, child.DesiredSize.Width), size.Height);
+        }
+
+        if (MainAxisAlignment == MainAxisAlignment.SpaceBetween)
+        {
+            size = new Size(size.Width, availableSize.Height);
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.SpaceAround)
+        {
+            size = new Size(size.Width, availableSize.Height);
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.SpaceEvenly)
+        {
+            size = new Size(size.Width, availableSize.Height);
+        }
+
+        return size;
+    }
+
+    protected override Size ArrangeOverride(Size finalSize)
+    {
+        double totalYSpace = 0;
+
+        foreach (var child in Children)
+        {
+            totalYSpace += child.DesiredSize.Height;
+        }
+
+        bool stretchPlacement = MainAxisAlignment is MainAxisAlignment.SpaceBetween or MainAxisAlignment.SpaceAround
+            or MainAxisAlignment.SpaceEvenly;
+        double spaceBetween = 0;
+        double spaceBeforeAfter = 0;
+        if (stretchPlacement)
+        {
+            double freeSpace = finalSize.Height - totalYSpace;
+            spaceBetween = freeSpace / (Children.Count - 1);
+
+            if (MainAxisAlignment == MainAxisAlignment.SpaceAround)
+            {
+                spaceBetween = freeSpace / Children.Count;
+                spaceBeforeAfter = spaceBetween / 2f;
+            }
+            else if (MainAxisAlignment == MainAxisAlignment.SpaceEvenly)
+            {
+                spaceBeforeAfter = freeSpace / (Children.Count + 1);
+                spaceBetween = (freeSpace - spaceBeforeAfter) / Children.Count;
+            }
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.Center)
+        {
+            spaceBeforeAfter = (finalSize.Height - totalYSpace) / 2;
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.End)
+        {
+            spaceBeforeAfter = finalSize.Height - totalYSpace;
+        }
+
+        double yOffset = spaceBeforeAfter;
+        foreach (var child in Children)
+        {
+            double xOffset = 0;
+            if (CrossAxisAlignment == CrossAxisAlignment.Center)
+            {
+                xOffset = finalSize.Width / 2f - child.DesiredSize.Width / 2f;
+            }
+            else if (CrossAxisAlignment == CrossAxisAlignment.End)
+            {
+                xOffset = finalSize.Width - child.DesiredSize.Width;
+            }
+
+            child.Arrange(new Rect(xOffset, yOffset, child.DesiredSize.Width, child.DesiredSize.Height));
+            yOffset += child.DesiredSize.Height + spaceBetween;
+        }
+
+        return finalSize;
+    }
+}

+ 96 - 0
src/PixiEditor.Extensions/UI/Panels/RowPanel.cs

@@ -0,0 +1,96 @@
+using Avalonia;
+using Avalonia.Controls;
+using PixiEditor.Extensions.FlyUI.Elements;
+
+namespace PixiEditor.Extensions.UI.Panels;
+
+public class RowPanel : Panel
+{
+    public MainAxisAlignment MainAxisAlignment { get; set; } = MainAxisAlignment.Start;
+    public CrossAxisAlignment CrossAxisAlignment { get; set; } = CrossAxisAlignment.Start;
+
+
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        Size size = new(0, 0);
+        foreach (var child in Children)
+        {
+            child.Measure(availableSize);
+            size += new Size(child.DesiredSize.Width, 0);
+            size = new Size(size.Width, Math.Max(size.Height, child.DesiredSize.Height));
+        }
+
+        if (MainAxisAlignment == MainAxisAlignment.SpaceBetween)
+        {
+            size = new Size(availableSize.Width, size.Height);
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.SpaceAround)
+        {
+            size = new Size(availableSize.Width, size.Height);
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.SpaceEvenly)
+        {
+            size = new Size (availableSize.Width, size.Height);
+        }
+
+        return size;
+    }
+
+    protected override Size ArrangeOverride(Size finalSize)
+    {
+        double totalXSpace = 0;
+
+        foreach (var child in Children)
+        {
+            totalXSpace += child.DesiredSize.Width;
+        }
+
+        bool stretchPlacement = MainAxisAlignment is MainAxisAlignment.SpaceBetween or MainAxisAlignment.SpaceAround
+            or MainAxisAlignment.SpaceEvenly;
+        double spaceBetween = 0;
+        double spaceBeforeAfter = 0;
+        if (stretchPlacement)
+        {
+            double freeSpace = finalSize.Width - totalXSpace;
+            spaceBetween = freeSpace / (Children.Count - 1);
+
+            if (MainAxisAlignment == MainAxisAlignment.SpaceAround)
+            {
+                spaceBetween = freeSpace / Children.Count;
+                spaceBeforeAfter = spaceBetween / 2f;
+            }
+            else if (MainAxisAlignment == MainAxisAlignment.SpaceEvenly)
+            {
+                spaceBeforeAfter = freeSpace / (Children.Count + 1);
+                spaceBetween = (freeSpace - spaceBeforeAfter) / Children.Count;
+            }
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.Center)
+        {
+            spaceBeforeAfter = (finalSize.Width - totalXSpace) / 2;
+        }
+        else if (MainAxisAlignment == MainAxisAlignment.End)
+        {
+            spaceBeforeAfter = finalSize.Width - totalXSpace;
+        }
+
+        double xOffset = spaceBeforeAfter;
+        foreach (var child in Children)
+        {
+            double yOffset = 0;
+            if (CrossAxisAlignment == CrossAxisAlignment.Center)
+            {
+                yOffset = finalSize.Height / 2f - child.DesiredSize.Height / 2f;
+            }
+            else if (CrossAxisAlignment == CrossAxisAlignment.End)
+            {
+                yOffset = finalSize.Height - child.DesiredSize.Height;
+            }
+
+            child.Arrange(new Rect(xOffset, yOffset, child.DesiredSize.Width, child.DesiredSize.Height));
+            xOffset += child.DesiredSize.Width + spaceBetween;
+        }
+
+        return finalSize;
+    }
+}

+ 2 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.AnimationRenderer.FFmpeg;
 using PixiEditor.AnimationRenderer.FFmpeg;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.CommonApi.IO;
 using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
@@ -123,6 +124,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IDocumentBuilder, FontDocumentBuilder>()
             .AddSingleton<IDocumentBuilder, FontDocumentBuilder>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<CommandProvider>()
             .AddSingleton<CommandProvider>()
+            .AddSingleton<IDocumentProvider, DocumentProvider>()
             .AddSingleton<ICommandProvider, CommandProvider>(x => x.GetRequiredService<CommandProvider>())
             .AddSingleton<ICommandProvider, CommandProvider>(x => x.GetRequiredService<CommandProvider>())
             .AddSingleton<IIconLookupProvider, DynamicResourceIconLookupProvider>()
             .AddSingleton<IIconLookupProvider, DynamicResourceIconLookupProvider>()
             // Palette Parsers
             // Palette Parsers

+ 1 - 1
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -115,7 +115,7 @@ internal class ClassicDesktopEntry
 
 
         ExtensionLoader extensionLoader = new ExtensionLoader(Paths.ExtensionPackagesPath, Paths.UserExtensionsPath);
         ExtensionLoader extensionLoader = new ExtensionLoader(Paths.ExtensionPackagesPath, Paths.UserExtensionsPath);
         //TODO: fetch from extension store
         //TODO: fetch from extension store
-        extensionLoader.AddOfficialExtension("pixieditor.supporterpack",
+        extensionLoader.AddOfficialExtension("pixieditor.founderspack",
             new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
             new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
         extensionLoader.AddOfficialExtension("pixieditor.beta", new OfficialExtensionData());
         extensionLoader.AddOfficialExtension("pixieditor.beta", new OfficialExtensionData());
         if (!safeMode)
         if (!safeMode)

+ 2 - 2
src/PixiEditor/Models/ExtensionServices/CommandProvider.cs

@@ -49,9 +49,9 @@ public class CommandProvider : ICommandProvider
         CommandController.Current.AddManagedCommand(basicCommand);
         CommandController.Current.AddManagedCommand(basicCommand);
     }
     }
 
 
-    private static KeyCombination ToKeyCombination(Shortcut shortcut)
+    private static KeyCombination ToKeyCombination(Shortcut? shortcut)
     {
     {
-        if (shortcut is { Key: 0, Modifiers: 0 })
+        if (shortcut is null or { Key: 0, Modifiers: 0 })
             return KeyCombination.None;
             return KeyCombination.None;
 
 
         return new KeyCombination((Key)shortcut.Key, (KeyModifiers)shortcut.Modifiers);
         return new KeyCombination((Key)shortcut.Key, (KeyModifiers)shortcut.Modifiers);

+ 21 - 0
src/PixiEditor/Models/ExtensionServices/DocumentProvider.cs

@@ -0,0 +1,21 @@
+using PixiEditor.Extensions.CommonApi.IO;
+using PixiEditor.Models.IO;
+using PixiEditor.ViewModels;
+using PixiEditor.ViewModels.SubViewModels;
+
+namespace PixiEditor.Models.ExtensionServices;
+
+internal class DocumentProvider : IDocumentProvider
+{
+    private FileViewModel fileViewModel;
+
+    public DocumentProvider(FileViewModel fileViewModel)
+    {
+        this.fileViewModel = fileViewModel;
+    }
+
+    public void ImportFile(string path, bool associatePath = true)
+    {
+        fileViewModel.OpenFromPath(path, associatePath);
+    }
+}

+ 1 - 1
src/PixiEditor/Models/ExtensionServices/WindowProvider.cs

@@ -41,7 +41,7 @@ public class WindowProvider : IWindowProvider
 
 
     public IPopupWindow CreatePopupWindow(string title, object body)
     public IPopupWindow CreatePopupWindow(string title, object body)
     {
     {
-        return new PopupWindow(new PixiEditorPopup { Title = new LocalizedString(title), Content = body });
+        return new PopupWindow(new PixiEditorPopup { Title = title, Content = body });
     }
     }
 
 
     public IPopupWindow GetWindow(BuiltInWindowType type)
     public IPopupWindow GetWindow(BuiltInWindowType type)

+ 5 - 4
tests/PixiEditor.Extensions.Tests/LayoutBuilderElementsTests.cs

@@ -1,5 +1,6 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
 using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.UI.Panels;
 
 
 namespace PixiEditor.Extensions.Test;
 namespace PixiEditor.Extensions.Test;
 
 
@@ -20,8 +21,8 @@ public class LayoutBuilderElementsTests
         Panel grid = (Panel)result;
         Panel grid = (Panel)result;
         Assert.Single(grid.Children);
         Assert.Single(grid.Children);
 
 
-        Assert.IsType<StackPanel>(grid.Children[0]);
-        Panel childGrid = (StackPanel)grid.Children[0];
+        Assert.IsType<RowPanel>(grid.Children[0]);
+        Panel childGrid = (RowPanel)grid.Children[0];
 
 
         Assert.Equal(Avalonia.Layout.HorizontalAlignment.Stretch, childGrid.HorizontalAlignment);
         Assert.Equal(Avalonia.Layout.HorizontalAlignment.Stretch, childGrid.HorizontalAlignment);
         Assert.Equal(Avalonia.Layout.VerticalAlignment.Stretch, childGrid.VerticalAlignment);
         Assert.Equal(Avalonia.Layout.VerticalAlignment.Stretch, childGrid.VerticalAlignment);
@@ -53,8 +54,8 @@ public class LayoutBuilderElementsTests
         Panel grid = (Panel)result;
         Panel grid = (Panel)result;
         Assert.Single(grid.Children);
         Assert.Single(grid.Children);
 
 
-        Assert.IsType<StackPanel>(grid.Children[0]);
-        Panel childGrid = (StackPanel)grid.Children[0];
+        Assert.IsType<ColumnPanel>(grid.Children[0]);
+        Panel childGrid = (ColumnPanel)grid.Children[0];
 
 
         Assert.Equal(Avalonia.Layout.HorizontalAlignment.Stretch, childGrid.HorizontalAlignment);
         Assert.Equal(Avalonia.Layout.HorizontalAlignment.Stretch, childGrid.HorizontalAlignment);
         Assert.Equal(Avalonia.Layout.VerticalAlignment.Stretch, childGrid.VerticalAlignment);
         Assert.Equal(Avalonia.Layout.VerticalAlignment.Stretch, childGrid.VerticalAlignment);

+ 6 - 5
tests/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs

@@ -2,6 +2,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Presenters;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.UI.Panels;
 using Button = PixiEditor.Extensions.FlyUI.Elements.Button;
 using Button = PixiEditor.Extensions.FlyUI.Elements.Button;
 
 
 namespace PixiEditor.Extensions.Test;
 namespace PixiEditor.Extensions.Test;
@@ -154,19 +155,19 @@ public class LayoutBuilderTests
         var native = testStatefulElement.BuildNative();
         var native = testStatefulElement.BuildNative();
 
 
         Assert.IsType<ContentPresenter>(native);
         Assert.IsType<ContentPresenter>(native);
-        Assert.IsType<StackPanel>((native as ContentPresenter).Content);
-        StackPanel panel = (native as ContentPresenter).Content as StackPanel;
+        Assert.IsType<ColumnPanel>((native as ContentPresenter).Content);
+        ColumnPanel panel = (native as ContentPresenter).Content as ColumnPanel;
 
 
         Assert.Equal(2, panel.Children.Count);
         Assert.Equal(2, panel.Children.Count);
 
 
         Assert.IsType<Avalonia.Controls.Button>(panel.Children[0]);
         Assert.IsType<Avalonia.Controls.Button>(panel.Children[0]);
-        Assert.IsType<StackPanel>(panel.Children[1]);
+        Assert.IsType<RowPanel>(panel.Children[1]);
 
 
-        Assert.Empty((panel.Children[1] as StackPanel).Children);
+        Assert.Empty((panel.Children[1] as RowPanel).Children);
         Assert.Empty(testStatefulElement.State.Rows);
         Assert.Empty(testStatefulElement.State.Rows);
 
 
         Avalonia.Controls.Button button = (Avalonia.Controls.Button)panel.Children[0];
         Avalonia.Controls.Button button = (Avalonia.Controls.Button)panel.Children[0];
-        StackPanel innerPanel = (StackPanel)panel.Children[1];
+        RowPanel innerPanel = (RowPanel)panel.Children[1];
 
 
         button.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
         button.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));