Pārlūkot izejas kodu

Added invoke command and permission system

Krzysztof Krysiński 3 mēneši atpakaļ
vecāks
revīzija
b8a25ee8ef
30 mainītis faili ar 490 papildinājumiem un 32 dzēšanām
  1. 6 0
      samples/PixiEditorExtensionSamples.sln
  2. 55 0
      samples/Sample8_CommandLibrary/CommandLibraryExtension.cs
  3. 9 0
      samples/Sample8_CommandLibrary/Program.cs
  4. 39 0
      samples/Sample8_CommandLibrary/Sample8_CommandLibrary.csproj
  5. 41 0
      samples/Sample8_CommandLibrary/extension.json
  6. 24 0
      samples/Sample8_Commands/CommandsSampleExtension.cs
  7. 1 1
      samples/Sample8_Commands/Program.cs
  8. 8 0
      src/PixiEditor.Extensions.CommonApi/Commands/ICommandProvider.cs
  9. 26 0
      src/PixiEditor.Extensions.CommonApi/DataContracts/CommandMetadata.proto
  10. 0 8
      src/PixiEditor.Extensions.CommonApi/Menu/ICommandProvider.cs
  11. 16 0
      src/PixiEditor.Extensions.CommonApi/ProtoAutogen/CommandMetadata.cs
  12. 19 9
      src/PixiEditor.Extensions.CommonApi/Utilities/PrefixedNameUtility.cs
  13. 10 1
      src/PixiEditor.Extensions.Sdk/Api/Commands/CommandProvider.cs
  14. 6 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.Commands.cs
  15. 1 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.cs
  16. 40 3
      src/PixiEditor.Extensions.WasmRuntime/Api/CommandApi.cs
  17. 5 2
      src/PixiEditor.Extensions.WasmRuntime/Api/Modules/CommandModule.cs
  18. 2 1
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  19. 8 0
      src/PixiEditor.Extensions/Commands/ICommandSupervisor.cs
  20. 5 2
      src/PixiEditor.Extensions/ExtensionServices.cs
  21. 5 1
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  22. 3 0
      src/PixiEditor/Models/Commands/CommandController.cs
  23. 3 0
      src/PixiEditor/Models/Commands/Commands/Command.cs
  24. 25 0
      src/PixiEditor/Models/Commands/Commands/CommandPermissions.cs
  25. 19 1
      src/PixiEditor/Models/ExtensionServices/CommandProvider.cs
  26. 78 0
      src/PixiEditor/Models/ExtensionServices/CommandSupervisor.cs
  27. 21 0
      src/PixiEditor/Models/ExtensionServices/ConsoleLogger.cs
  28. 0 1
      src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs
  29. 3 2
      src/PixiEditor/Views/Dialogs/Debugging/CommandDebugPopup.axaml
  30. 12 0
      src/PixiEditor/Views/Dialogs/Debugging/CommandDebugPopup.axaml.cs

+ 6 - 0
samples/PixiEditorExtensionSamples.sln

@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample7_FlyUI", "Sample7_Fl
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample8_Commands", "Sample8_Commands\Sample8_Commands.csproj", "{25DA4758-9F82-494E-96A3-B9C48637C0E0}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample8_CommandLibrary", "Sample8_CommandLibrary\Sample8_CommandLibrary.csproj", "{3559A288-DF82-4429-B23C-CFF9E55B372E}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -68,6 +70,10 @@ Global
 		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{FD9B4C32-4D2E-410E-BC6B-787779BEB6E2} = {7CC35BC4-829F-4EF4-8EB6-E1D46206E7DC}

+ 55 - 0
samples/Sample8_CommandLibrary/CommandLibraryExtension.cs

@@ -0,0 +1,55 @@
+using PixiEditor.Extensions.CommonApi.Commands;
+using PixiEditor.Extensions.Sdk;
+
+namespace Sample8_CommandLibrary;
+
+public class CommandLibraryExtension : PixiEditorExtension
+{
+    public override void OnInitialized()
+    {
+        CommandMetadata publicCommand = new CommandMetadata("PrintHelloWorld")
+        {
+            // All extensions can invoke this command
+            InvokePermissions = InvokePermissions.Public
+        };
+
+        CommandMetadata internalCommand = new CommandMetadata("PrintHelloWorldFamily")
+        {
+            // All extensions with unique name starting with "yourCompany" can invoke this command
+            InvokePermissions = InvokePermissions.Family
+        };
+
+        CommandMetadata privateCommand = new CommandMetadata("PrintHelloWorldPrivate")
+        {
+            // Only this extension can invoke this command
+            InvokePermissions = InvokePermissions.Owner
+        };
+
+        CommandMetadata explicitCommand = new CommandMetadata("PrintHelloWorldExplicit")
+        {
+            // Only this extension and the ones listed in ExplicitlyAllowedExtensions can invoke this command
+            InvokePermissions = InvokePermissions.Explicit,
+            ExplicitlyAllowedExtensions = "yourCompany.Samples.Commands" // You can put multiple extensions by separating with ;
+        };
+
+        Api.Commands.RegisterCommand(publicCommand, () =>
+        {
+            Api.Logger.Log("Hello World from public command!");
+        });
+
+        Api.Commands.RegisterCommand(internalCommand, () =>
+        {
+            Api.Logger.Log("Hello World from internal command!");
+        });
+
+        Api.Commands.RegisterCommand(privateCommand, () =>
+        {
+            Api.Logger.Log("Hello World from private command!");
+        });
+
+        Api.Commands.RegisterCommand(explicitCommand, () =>
+        {
+            Api.Logger.Log("Hello World from explicit command!");
+        });
+    }
+}

+ 9 - 0
samples/Sample8_CommandLibrary/Program.cs

@@ -0,0 +1,9 @@
+namespace Sample8_CommandLibrary;
+
+public static class Program
+{
+    public static void Main()
+    {
+
+    }
+}

+ 39 - 0
samples/Sample8_CommandLibrary/Sample8_CommandLibrary.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
+        <OutputType>Exe</OutputType>
+        <PublishTrimmed>true</PublishTrimmed>
+        <WasmSingleFileBundle>true</WasmSingleFileBundle>
+        <GenerateExtensionPackage>true</GenerateExtensionPackage>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
+        <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+        <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
+        <RootNamespace>Sample8_CommandLibrary</RootNamespace>
+    </PropertyGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Remove="extension.json"/>
+        <Content Include="extension.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Localization\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
+
+
+</Project>

+ 41 - 0
samples/Sample8_CommandLibrary/extension.json

@@ -0,0 +1,41 @@
+{
+  "displayName": "Sample Extension - Command Library",
+  "uniqueName": "yourCompany.Samples.CommandLibrary",
+  "description": "Commands Library that can be invoked by other extensions for PixiEditor",
+  "version": "1.0.0",
+  "localization": {
+    "languages": [
+      {
+        "name": "English",
+        "code": "en",
+        "localeFileName": "Localization/en.json"
+      },
+      {
+        "name": "Polish",
+        "code": "pl",
+        "localeFileName": "Localization/pl.json"
+      }
+    ]
+  },
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "contributors": [
+    {
+      "name": "flabbet",
+      "email": "[email protected]",
+      "website": "https://github.com/flabbet"
+    }
+  ],
+  "license": "MIT",
+  "categories": [
+    "Extension"
+  ]
+}

+ 24 - 0
samples/Sample8_Commands/CommandsSampleExtension.cs

@@ -47,5 +47,29 @@ public class CommandsSampleExtension : PixiEditorExtension
             clickedCount++;
             Api.Logger.Log($"Clicked {clickedCount} times");
         });
+
+
+        Api.Commands.InvokeCommand("PixiEditor.File.New");
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorld"))
+        {
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorld");
+        }
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorldFamily"))
+        {
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorldFamily");
+        }
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorldPrivate"))
+        {
+            // This will log an error.
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorldPrivate");
+        }
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorldExplicit"))
+        {
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorldExplicit");
+        }
     }
 }

+ 1 - 1
samples/Sample8_Commands/Program.cs

@@ -1,4 +1,4 @@
-namespace Sample8_Menu;
+namespace Sample8_Commands;
 
 public static class Program
 {

+ 8 - 0
src/PixiEditor.Extensions.CommonApi/Commands/ICommandProvider.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Extensions.CommonApi.Commands;
+
+public interface ICommandProvider
+{
+    public void RegisterCommand(CommandMetadata command, Action execute, Func<bool>? canExecute = null);
+    public void InvokeCommand(string commandName);
+    public bool CommandExists(string commandName);
+}

+ 26 - 0
src/PixiEditor.Extensions.CommonApi/DataContracts/CommandMetadata.proto

@@ -13,5 +13,31 @@ message CommandMetadata
   string Icon = 5;
   string MenuItemPath = 6;
   int32 Order = 7;
+  InvokePermissions InvokePermissions = 8;
+  string ExplicitlyAllowedExtensions = 9;
+}
 
+enum InvokePermissions
+{
+
+  /// <summary>
+  ///     Only the registering extension can use this command.
+  /// </summary>
+    Owner = 0;
+
+  /// <summary>
+  ///     Only extensions explicitly whitelisted by the registering extension can use this command.
+  /// </summary>
+    Explicit = 1;
+
+  /// <summary>
+  ///     Only extensions that are part of the same family can use this command. A family is a group under the same
+  ///     unique name prefix.
+  /// </summary>
+    Family = 2;
+
+  /// <summary>
+  ///     Any extension can use this command.
+  /// </summary>
+    Public = 3;
 }

+ 0 - 8
src/PixiEditor.Extensions.CommonApi/Menu/ICommandProvider.cs

@@ -1,8 +0,0 @@
-using PixiEditor.Extensions.CommonApi.Commands;
-
-namespace PixiEditor.Extensions.CommonApi.Menu;
-
-public interface ICommandProvider
-{
-    public void RegisterCommand(CommandMetadata command, Action execute, Func<bool>? canExecute = null);
-}

+ 16 - 0
src/PixiEditor.Extensions.CommonApi/ProtoAutogen/CommandMetadata.cs

@@ -42,6 +42,22 @@ namespace PixiEditor.Extensions.CommonApi.Commands
         [global::ProtoBuf.ProtoMember(7)]
         public int Order { get; set; }
 
+        [global::ProtoBuf.ProtoMember(8)]
+        public InvokePermissions InvokePermissions { get; set; }
+
+        [global::ProtoBuf.ProtoMember(9)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string ExplicitlyAllowedExtensions { get; set; } = "";
+
+    }
+
+    [global::ProtoBuf.ProtoContract()]
+    public enum InvokePermissions
+    {
+        Owner = 0,
+        Explicit = 1,
+        Family = 2,
+        Public = 3,
     }
 
 }

+ 19 - 9
src/PixiEditor.Extensions.CommonApi/Utilities/PrefixedNameUtility.cs

@@ -12,14 +12,14 @@ public static class PrefixedNameUtility
     public static string ToPixiEditorRelativePreferenceName(string uniqueName, string name)
     {
         string[] splitted = name.Split(":");
-        
+
         string finalName = $"{uniqueName}:{name}";
-        
+
         if (splitted.Length == 2)
         {
             finalName = name;
-            
-            if(splitted[0].Equals("pixieditor", StringComparison.CurrentCultureIgnoreCase))
+
+            if (splitted[0].Equals("pixieditor", StringComparison.CurrentCultureIgnoreCase))
             {
                 finalName = splitted[1];
             }
@@ -42,7 +42,7 @@ public static class PrefixedNameUtility
             return preferenceName[(extensionUniqueName.Length + 1)..];
         }
 
-        if(preferenceName.Split(":").Length == 1)
+        if (preferenceName.Split(":").Length == 1)
         {
             return $"pixieditor:{preferenceName}";
         }
@@ -50,13 +50,23 @@ public static class PrefixedNameUtility
         return preferenceName;
     }
 
-    public static string ToCommandUniqueName(string extensionUniqueName, string metadataUniqueName)
+    public static string ToCommandUniqueName(string extensionUniqueName, string commandUniqueName, bool allowAlreadyPrefixed)
     {
-        if (metadataUniqueName.StartsWith(extensionUniqueName))
+        if (commandUniqueName.Contains(':'))
+        {
+            if (allowAlreadyPrefixed)
+            {
+                return commandUniqueName;
+            }
+
+            throw new ArgumentException($"Command name '{commandUniqueName}' already contains a prefix. Which is not allowed.");
+        }
+
+        if (commandUniqueName.StartsWith(extensionUniqueName) || commandUniqueName.StartsWith("PixiEditor."))
         {
-            return metadataUniqueName;
+            return commandUniqueName;
         }
 
-        return $"{extensionUniqueName}:{metadataUniqueName}";
+        return $"{extensionUniqueName}:{commandUniqueName}";
     }
 }

+ 10 - 1
src/PixiEditor.Extensions.Sdk/Api/Commands/CommandProvider.cs

@@ -1,5 +1,4 @@
 using PixiEditor.Extensions.CommonApi.Commands;
-using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.Sdk.Bridge;
 
 namespace PixiEditor.Extensions.Sdk.Api.Commands;
@@ -28,6 +27,16 @@ public class CommandProvider : ICommandProvider
         Interop.RegisterCommand(command);
     }
 
+    public void InvokeCommand(string commandName)
+    {
+        Native.invoke_command(commandName);
+    }
+
+    public bool CommandExists(string commandName)
+    {
+        return Native.command_exists(commandName);
+    }
+
     private void OnCommandInvoked(string uniqueName)
     {
         if (_commands.TryGetValue(uniqueName, out var command))

+ 6 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Native.Commands.cs

@@ -9,6 +9,12 @@ internal static partial class Native
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void register_command(IntPtr metadataPtr, int length);
 
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void invoke_command(string commandName);
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern bool command_exists(string commandName);
+
     [ApiExport("command_invoked")]
     internal static void OnCommandInvoked(string uniqueName)
     {

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

@@ -60,4 +60,5 @@ internal static partial class Native
 
     [MethodImpl(MethodImplOptions.InternalCall)]
     public static extern string to_resources_full_path(string value);
+
 }

+ 40 - 3
src/PixiEditor.Extensions.WasmRuntime/Api/CommandApi.cs

@@ -24,8 +24,45 @@ internal class CommandApi : ApiGroupHandler
             commandModule.InvokeCommandInvoked(originalName);
         }
 
-        string prefixed = PrefixedNameUtility.ToCommandUniqueName(Extension.Metadata.UniqueName, metadata.UniqueName);
-        metadata.UniqueName = prefixed;
-        Api.Commands.RegisterCommand(metadata, InvokeCommandInvoked);
+        try
+        {
+            string prefixed =
+                PrefixedNameUtility.ToCommandUniqueName(Extension.Metadata.UniqueName, metadata.UniqueName, false);
+            metadata.UniqueName = prefixed;
+            Api.Commands.RegisterCommand(metadata, InvokeCommandInvoked);
+        }
+        catch (ArgumentException ex)
+        {
+            Api.Logger.LogError(ex.Message);
+        }
+    }
+
+    [ApiFunction("command_exists")]
+    internal bool CommandExists(string commandName)
+    {
+        CommandModule commandModule = Extension.GetModule<CommandModule>();
+        return commandModule.CommandProvider.CommandExists(commandName);
+    }
+
+    [ApiFunction("invoke_command")]
+    internal void InvokeCommand(string commandName)
+    {
+        CommandModule commandModule = Extension.GetModule<CommandModule>();
+
+        string prefixedName = PrefixedNameUtility.ToCommandUniqueName(Extension.Metadata.UniqueName, commandName, true);
+
+        if (!commandModule.CommandProvider.CommandExists(prefixedName))
+        {
+            return;
+        }
+
+        if (commandModule.CommandSupervisor.ValidateCommandPermissions(prefixedName, Extension))
+        {
+            Api.Commands.InvokeCommand(prefixedName);
+        }
+        else
+        {
+            Api.Logger.LogError($"Command {prefixedName} is not accessible from {Metadata.UniqueName} extension.");
+        }
     }
 }

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

@@ -1,14 +1,17 @@
-using PixiEditor.Extensions.CommonApi.Menu;
+using PixiEditor.Extensions.Commands;
+using PixiEditor.Extensions.CommonApi.Commands;
 
 namespace PixiEditor.Extensions.WasmRuntime.Api.Modules;
 
 internal class CommandModule : ApiModule
 {
     public ICommandProvider CommandProvider { get; }
+    public ICommandSupervisor CommandSupervisor { get; }
 
-    public CommandModule(WasmExtensionInstance extension, ICommandProvider commandProvider) : base(extension)
+    public CommandModule(WasmExtensionInstance extension, ICommandProvider commandProvider, ICommandSupervisor supervisor) : base(extension)
     {
         CommandProvider = commandProvider;
+        CommandSupervisor = supervisor;
     }
 
     internal void InvokeCommandInvoked(string uniqueName)

+ 2 - 1
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -2,6 +2,7 @@ using System.Runtime.InteropServices;
 using System.Text;
 using Avalonia.Controls;
 using Avalonia.Threading;
+using PixiEditor.Extensions.Commands;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.FlyUI;
 using PixiEditor.Extensions.FlyUI.Elements;
@@ -65,7 +66,7 @@ public partial class WasmExtensionInstance : Extension
     protected override void OnInitialized()
     {
         modules.Add(new PreferencesModule(this, Api.Preferences));
-        modules.Add(new CommandModule(this, Api.Commands));
+        modules.Add(new CommandModule(this, Api.Commands, (ICommandSupervisor)Api.Services.GetService(typeof(ICommandSupervisor))));
         LayoutBuilder = new LayoutBuilder((ElementMap)Api.Services.GetService(typeof(ElementMap)));
 
         //SetElementMap();

+ 8 - 0
src/PixiEditor.Extensions/Commands/ICommandSupervisor.cs

@@ -0,0 +1,8 @@
+using PixiEditor.Extensions.CommonApi.Commands;
+
+namespace PixiEditor.Extensions.Commands;
+
+public interface ICommandSupervisor
+{
+    public bool ValidateCommandPermissions(string commandName, Extension invoker);
+}

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

@@ -1,6 +1,8 @@
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.Commands;
+using PixiEditor.Extensions.CommonApi.Commands;
 using PixiEditor.Extensions.CommonApi.IO;
-using PixiEditor.Extensions.CommonApi.Menu;
+using PixiEditor.Extensions.CommonApi.Logging;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.Windowing;
@@ -15,9 +17,10 @@ public class ExtensionServices
     public IFileSystemProvider? FileSystem => Services.GetService<IFileSystemProvider>();
     public IPreferences? Preferences => Services.GetService<IPreferences>();
     public ICommandProvider? Commands => Services.GetService<ICommandProvider>();
-    
     public IPalettesProvider? Palettes => Services.GetService<IPalettesProvider>();
     public IDocumentProvider Documents => Services.GetService<IDocumentProvider>();
+    public ICommandSupervisor CommandSupervisor => Services.GetService<ICommandSupervisor>();
+    public ILogger Logger => Services.GetService<ILogger>();
 
     public ExtensionServices(IServiceProvider services)
     {

+ 5 - 1
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -4,9 +4,11 @@ using System.Reflection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.AnimationRenderer.FFmpeg;
+using PixiEditor.Extensions.Commands;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.CommonApi.Commands;
 using PixiEditor.Extensions.CommonApi.IO;
-using PixiEditor.Extensions.CommonApi.Menu;
+using PixiEditor.Extensions.CommonApi.Logging;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
@@ -209,5 +211,7 @@ internal static class ServiceCollectionHelpers
 
                 return elementMap;
             })
+            .AddSingleton<ICommandSupervisor, CommandSupervisor>()
+            .AddSingleton<ILogger, ConsoleLogger>()
             .AddSingleton<IFileSystemProvider, FileSystemProvider>();
 }

+ 3 - 0
src/PixiEditor/Models/Commands/CommandController.cs

@@ -14,6 +14,7 @@ using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.CommandContext;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Handlers;
@@ -288,6 +289,7 @@ internal class CommandController
                                 Description = attribute.Description,
                                 Icon = attribute.Icon,
                                 IconEvaluator = xIcon,
+                                InvokePermissions = CommandPermissions.Public,
                                 DefaultShortcut = AdjustForOS(attribute.GetShortcut(), validCustomShortcut),
                                 Shortcut =
                                     GetShortcut(name, AdjustForOS(attribute.GetShortcut(), validCustomShortcut),
@@ -339,6 +341,7 @@ internal class CommandController
                                 InternalName = menu.InternalName,
                                 DisplayName = menu.DisplayName,
                                 Description = menu.DisplayName,
+                                InvokePermissions = CommandPermissions.Public,
                                 IconEvaluator = IconEvaluator.Default,
                                 DefaultShortcut = AdjustForOS(menu.GetShortcut(), validCustomShortcut),
                                 Shortcut = GetShortcut(name, AdjustForOS(attribute.GetShortcut(), validCustomShortcut),

+ 3 - 0
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -48,6 +48,9 @@ internal abstract partial class Command : PixiObservableObject
 
     public int MenuItemOrder { get; init; } = 100;
 
+    public CommandPermissions InvokePermissions { get; init; } = CommandPermissions.Owner;
+    public string[]? ExplicitPermissions { get; init; }
+
     public event ShortcutChangedEventHandler ShortcutChanged;
     public event Action CanExecuteChanged;
 

+ 25 - 0
src/PixiEditor/Models/Commands/Commands/CommandPermissions.cs

@@ -0,0 +1,25 @@
+namespace PixiEditor.Models.Commands.Commands;
+
+public enum CommandPermissions
+{
+    /// <summary>
+    ///     Only the registering extension can use this command.
+    /// </summary>
+    Owner,
+
+    /// <summary>
+    ///     Only extensions explicitly whitelisted by the registering extension can use this command.
+    /// </summary>
+    Explicit,
+
+    /// <summary>
+    ///     Only extensions that are part of the same family can use this command. A family is a group under the same
+    ///     unique name prefix.
+    /// </summary>
+    Family,
+
+    /// <summary>
+    ///     Any extension can use this command.
+    /// </summary>
+    Public,
+}

+ 19 - 1
src/PixiEditor/Models/ExtensionServices/CommandProvider.cs

@@ -1,5 +1,4 @@
 using PixiEditor.Extensions.CommonApi.Commands;
-using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
@@ -43,12 +42,31 @@ public class CommandProvider : ICommandProvider
             Description = command.Description,
             MenuItemOrder = command.Order,
             DefaultShortcut = shortcut,
+            InvokePermissions = (CommandPermissions)command.InvokePermissions,
+            ExplicitPermissions = command.ExplicitlyAllowedExtensions?.Split(';'),
             IconEvaluator = IconEvaluator.Default
         };
 
         CommandController.Current.AddManagedCommand(basicCommand);
     }
 
+    public void InvokeCommand(string commandName)
+    {
+        if (CommandController.Current.Commands.ContainsKey(commandName))
+        {
+            var command = CommandController.Current.Commands[commandName];
+            if (command.CanExecute())
+            {
+                command.Execute();
+            }
+        }
+    }
+
+    public bool CommandExists(string commandName)
+    {
+        return CommandController.Current.Commands.ContainsKey(commandName);
+    }
+
     private static KeyCombination ToKeyCombination(Shortcut? shortcut)
     {
         if (shortcut is null or { Key: 0, Modifiers: 0 })

+ 78 - 0
src/PixiEditor/Models/ExtensionServices/CommandSupervisor.cs

@@ -0,0 +1,78 @@
+using PixiEditor.Extensions;
+using PixiEditor.Extensions.Commands;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
+
+namespace PixiEditor.Models.ExtensionServices;
+
+internal class CommandSupervisor : ICommandSupervisor
+{
+    public bool ValidateCommandPermissions(string commandName, Extension invoker)
+    {
+        if (CommandController.Current.Commands.ContainsKey(commandName))
+        {
+            var command = CommandController.Current.Commands[commandName];
+
+            if (IsOwnedByExtension(command, invoker))
+            {
+                return true;
+            }
+
+            if (command.InvokePermissions == CommandPermissions.Public)
+            {
+                return true;
+            }
+
+            if (command.InvokePermissions == CommandPermissions.Explicit)
+            {
+                if (command.ExplicitPermissions == null)
+                {
+                    return false;
+                }
+
+                foreach (var extension in command.ExplicitPermissions)
+                {
+                    if (invoker.Metadata.UniqueName == extension)
+                    {
+                        return true;
+                    }
+                }
+            }
+            else if (command.InvokePermissions == CommandPermissions.Family)
+            {
+                string[] split = command.InternalName.Split('.');
+                if (split.Length < 2)
+                {
+                    return false;
+                }
+
+                string family = split[0];
+
+                if (invoker.Metadata.UniqueName.StartsWith(family))
+                {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private bool IsOwnedByExtension(Command command, Extension invoker)
+    {
+        string[] split = command.InternalName.Split(':');
+        if (split.Length < 2)
+        {
+            return false;
+        }
+
+        string family = split[0];
+
+        if (invoker.Metadata.UniqueName == family)
+        {
+            return true;
+        }
+
+        return false;
+    }
+}

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

@@ -0,0 +1,21 @@
+using PixiEditor.Extensions.CommonApi.Logging;
+
+namespace PixiEditor.Models.ExtensionServices;
+
+internal class ConsoleLogger : ILogger
+{
+    public void Log(string message)
+    {
+        Console.WriteLine(message);
+    }
+
+    public void LogError(string message)
+    {
+        Console.WriteLine($"Error: {message}");
+    }
+
+    public void LogWarning(string message)
+    {
+        Console.WriteLine($"Warning: {message}");
+    }
+}

+ 0 - 1
src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs

@@ -5,7 +5,6 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Data;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Extensions.Common.Localization;
-using PixiEditor.Extensions.CommonApi.Menu;
 using PixiEditor.Extensions.UI;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Evaluators;

+ 3 - 2
src/PixiEditor/Views/Dialogs/Debugging/CommandDebugPopup.axaml

@@ -16,11 +16,12 @@
             <RowDefinition />
         </Grid.RowDefinitions>
 
-        <StackPanel Orientation="Horizontal" Margin="5">
+        <StackPanel Orientation="Horizontal" Margin="5" Spacing="10">
             <Button Content="Export list"
                     Command="{xaml:Command PixiEditor.Debug.DumpAllCommands}" Width="100" />
-            <CheckBox Content="Show only with issues" IsCheckedChanged="ShowOnlyWithIssues_OnIsCheckedChanged" Margin="10 0"/>
+            <CheckBox Content="Show only with issues" IsCheckedChanged="ShowOnlyWithIssues_OnIsCheckedChanged"/>
             <CheckBox Content="Show only without icons" IsCheckedChanged="ShowOnlyWithoutIcons_OnIsCheckedChanged"/>
+            <TextBox Width="150" Watermark="Search" TextChanged="TextBox_OnTextChanged"/>
         </StackPanel>
 
         <ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="1">

+ 12 - 0
src/PixiEditor/Views/Dialogs/Debugging/CommandDebugPopup.axaml.cs

@@ -168,4 +168,16 @@ public partial class CommandDebugPopup : PixiEditorPopup
             }
         }
     }
+
+    private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e)
+    {
+        if (sender is TextBox textBox)
+        {
+            string filter = textBox.Text.ToLower();
+
+            Commands = new ObservableCollection<CommandDebug>(allCommands
+                .Where(x => x.Command.InternalName.ToLower().Contains(filter) ||
+                            x.Command.DisplayName.ToString().ToLower().Contains(filter)));
+        }
+    }
 }