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>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <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>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <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.Api.FlyUI;
 using PixiEditor.Extensions.Sdk.Api.Window;
 
 namespace FlyUISample;
 
+[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "FlyUI style")]
 public class WindowContentElement : StatelessElement
 {
     public PopupWindow Window { get; set; }
+
     public override CompiledControl BuildNative()
     {
         Layout layout = new Layout(body:
             new Container(margin: Edges.All(25), child:
                 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 Edges Thickness { get; set; }
-    
+
     public Edges CornerRadius { get; set; }
-    
+
     public Edges Padding { 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;
         Color = color;
@@ -21,6 +29,9 @@ public class Border : SingleChildLayoutElement
         CornerRadius = cornerRadius;
         Padding = padding;
         Margin = margin;
+        Width = width;
+        Height = height;
+        BackgroundColor = backgroundColor;
     }
 
     public override CompiledControl BuildNative()
@@ -33,6 +44,9 @@ public class Border : SingleChildLayoutElement
         control.AddProperty(CornerRadius);
         control.AddProperty(Padding);
         control.AddProperty(Margin);
+        control.AddProperty(Width);
+        control.AddProperty(Height);
+        control.AddProperty(BackgroundColor);
 
         return control;
     }

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

@@ -2,14 +2,31 @@
 
 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)
     {
+        MainAxisAlignment = MainAxisAlignment.Start;
+        CrossAxisAlignment = CrossAxisAlignment.Start;
         Children = new List<LayoutElement>(children);
     }
-    
+
     public override CompiledControl BuildNative()
     {
         CompiledControl control = new CompiledControl(UniqueId, "Column");
+        control.AddProperty(MainAxisAlignment);
+        control.AddProperty(CrossAxisAlignment);
         control.Children.AddRange(Children.Where(x => x != null).Select(x => x.BuildNative()));
 
         return control;

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

@@ -2,14 +2,31 @@
 
 public class Row : MultiChildLayoutElement
 {
+    public MainAxisAlignment MainAxisAlignment { get; set; }
+    public CrossAxisAlignment CrossAxisAlignment { get; set; }
+
     public Row(params 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()
     {
         CompiledControl control = new CompiledControl(UniqueId, "Row");
+        control.AddProperty(MainAxisAlignment);
+        control.AddProperty(CrossAxisAlignment);
         control.Children.AddRange(Children.Select(x => x.BuildNative()));
 
         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);
         
         SubscribeToEvents(compiledControl);
-        return new PopupWindow(handle) { Title = title };
+        return new PopupWindow(handle);
     }
 
     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.Sdk.Api;
 using PixiEditor.Extensions.Sdk.Api.Commands;
+using PixiEditor.Extensions.Sdk.Api.IO;
 using PixiEditor.Extensions.Sdk.Api.Logging;
 using PixiEditor.Extensions.Sdk.Api.Palettes;
 using PixiEditor.Extensions.Sdk.Api.UserPreferences;
@@ -15,6 +16,7 @@ public class PixiEditorApi
     public Preferences Preferences { get; }
     public PalettesProvider Palettes { get; }
     public CommandProvider Commands { get; }
+    public DocumentProvider Documents { get; }
 
     public PixiEditorApi()
     {
@@ -23,5 +25,6 @@ public class PixiEditorApi
         Preferences = new Preferences();
         Palettes = new PalettesProvider();
         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
 {
     [ApiFunction("to_resources_full_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;
     }
 }

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

@@ -1,5 +1,6 @@
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.Async;
+using PixiEditor.Extensions.CommonApi.Utilities;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Extensions.WasmRuntime.Utilities;
@@ -13,7 +14,8 @@ internal class WindowingApi : ApiGroupHandler
     public int CreatePopupWindow(string title, Span<byte> bodySpan)
     {
         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);
         return handle;
@@ -37,8 +39,9 @@ internal class WindowingApi : ApiGroupHandler
     [ApiFunction("set_window_title")]
     public void SetWindowTitle(int handle, string title)
     {
+        string localizedTitleKey = LocalizedString.FirstValidKey($"{Extension.Metadata.UniqueName}:{title}", title);
         var window = NativeObjectManager.GetObject<PopupWindow>(handle);
-        window.Title = title;
+        window.Title = localizedTitleKey;
     }
 
     [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 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 PixiEditor.Extensions.CommonApi.IO;
 using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
@@ -16,6 +17,7 @@ public class ExtensionServices
     public ICommandProvider? Commands => Services.GetService<ICommandProvider>();
     
     public IPalettesProvider? Palettes => Services.GetService<IPalettesProvider>();
+    public IDocumentProvider Documents => Services.GetService<IDocumentProvider>();
 
     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)
         {
-            return new Bitmap(path);
+            if (File.Exists(path))
+            {
+                return new Bitmap(path);
+            }
         }
         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
 {
     private Avalonia.Controls.Border border;
-    
-    private Edges _thickness;
-    private Color _color;
+
+    private Edges thickness;
+    private Color color;
     private Edges cornerRadius;
     private Edges padding;
     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 Padding { get => padding; set => SetField(ref padding, 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()
     {
         border = new Avalonia.Controls.Border();
-        
+
         border.ClipToBounds = true;
-        
+
         if (Child != null)
         {
             border.Child = Child.BuildNative();
         }
-        
+
         Binding colorBinding = new Binding()
         {
-            Source = this,
-            Path = nameof(Color),
-            Converter = new ColorToAvaloniaBrushConverter()
+            Source = this, Path = nameof(Color), Converter = new ColorToAvaloniaBrushConverter()
         };
-        
+
         Binding edgesBinding = new Binding()
         {
-            Source = this,
-            Path = nameof(Thickness),
-            Converter = new EdgesToThicknessConverter()
+            Source = this, Path = nameof(Thickness), Converter = new EdgesToThicknessConverter()
         };
-        
+
         Binding cornerRadiusBinding = new Binding()
         {
-            Source = this,
-            Path = nameof(CornerRadius),
-            Converter = new EdgesToCornerRadiusConverter()
+            Source = this, Path = nameof(CornerRadius), Converter = new EdgesToCornerRadiusConverter()
         };
-        
+
         Binding paddingBinding = new Binding()
         {
-            Source = this,
-            Path = nameof(Padding),
-            Converter = new EdgesToThicknessConverter()
+            Source = this, Path = nameof(Padding), Converter = new EdgesToThicknessConverter()
         };
-        
+
         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.BorderThicknessProperty, edgesBinding);
         border.Bind(Avalonia.Controls.Border.CornerRadiusProperty, cornerRadiusBinding);
         border.Bind(Decorator.PaddingProperty, paddingBinding);
         border.Bind(Layoutable.MarginProperty, marginBinding);
-        
+
         return border;
     }
 
@@ -97,6 +106,9 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         yield return CornerRadius;
         yield return Padding;
         yield return Margin;
+        yield return BackgroundColor;
+        yield return Width;
+        yield return Height;
     }
 
     public void DeserializeProperties(ImmutableList<object> values)
@@ -106,5 +118,11 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         CornerRadius = (Edges)values.ElementAtOrDefault(2, default(Edges));
         Padding = (Edges)values.ElementAtOrDefault(3, 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 Avalonia.Controls;
 using Avalonia.Layout;
 using Avalonia.Threading;
+using PixiEditor.Extensions.UI.Panels;
 
 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()
     {
@@ -44,15 +61,28 @@ public class Column : MultiChildLayoutElement
 
     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()));
 
         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.Layout;
 using Avalonia.Threading;
+using PixiEditor.Extensions.UI.Panels;
 
 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()
     {
     }
@@ -43,15 +62,32 @@ public class Row : MultiChildLayoutElement
 
     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()));
 
         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]
 [Newtonsoft.Json.JsonConverter(typeof(JsonEnumFlagConverter))]
 public enum ExtensionPermissions
 {
     None = 0,
-    
+
     /// <summary>
     ///     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).
     /// </summary>
     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.FFmpeg;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.CommonApi.IO;
 using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
@@ -123,6 +124,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IDocumentBuilder, FontDocumentBuilder>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<CommandProvider>()
+            .AddSingleton<IDocumentProvider, DocumentProvider>()
             .AddSingleton<ICommandProvider, CommandProvider>(x => x.GetRequiredService<CommandProvider>())
             .AddSingleton<IIconLookupProvider, DynamicResourceIconLookupProvider>()
             // Palette Parsers

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

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

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

@@ -49,9 +49,9 @@ public class CommandProvider : ICommandProvider
         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 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)
     {
-        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)

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

@@ -1,5 +1,6 @@
 using Avalonia.Controls;
 using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.UI.Panels;
 
 namespace PixiEditor.Extensions.Test;
 
@@ -20,8 +21,8 @@ public class LayoutBuilderElementsTests
         Panel grid = (Panel)result;
         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.VerticalAlignment.Stretch, childGrid.VerticalAlignment);
@@ -53,8 +54,8 @@ public class LayoutBuilderElementsTests
         Panel grid = (Panel)result;
         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.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.Interactivity;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.UI.Panels;
 using Button = PixiEditor.Extensions.FlyUI.Elements.Button;
 
 namespace PixiEditor.Extensions.Test;
@@ -154,19 +155,19 @@ public class LayoutBuilderTests
         var native = testStatefulElement.BuildNative();
 
         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.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);
 
         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));