Browse Source

Added trackable commands

CPKreuz 1 year ago
parent
commit
4498dc07f3

+ 4 - 0
src/PixiEditor/Models/AnalyticsAPI/Analytics.cs

@@ -1,5 +1,6 @@
 using System.Reflection;
 using System.Reflection;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
@@ -38,6 +39,9 @@ public static class Analytics
 
 
     internal static AnalyticEvent SendSwitchToTool(IToolHandler? newTool, IToolHandler? oldTool) =>
     internal static AnalyticEvent SendSwitchToTool(IToolHandler? newTool, IToolHandler? oldTool) =>
         SendEvent(AnalyticEventTypes.SwitchTool, ("NewTool", newTool?.ToolName), ("OldTool", oldTool?.ToolName));
         SendEvent(AnalyticEventTypes.SwitchTool, ("NewTool", newTool?.ToolName), ("OldTool", oldTool?.ToolName));
+
+    internal static AnalyticEvent SendCommand(string commandName, ICommandExecutionSourceInfo? source) =>
+        SendEvent(AnalyticEventTypes.GeneralCommand, ("CommandName", commandName), ("Source", source));
     
     
     private static AnalyticEvent SendEvent(string name, params (string, object)[] data) =>
     private static AnalyticEvent SendEvent(string name, params (string, object)[] data) =>
         SendEvent(name, data.ToDictionary());
         SendEvent(name, data.ToDictionary());

+ 5 - 0
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -38,6 +38,11 @@ internal partial class Command
         /// Gets or sets the icon.
         /// Gets or sets the icon.
         /// </summary>
         /// </summary>
         public string Icon { get; set; }
         public string Icon { get; set; }
+        
+        /// <summary>
+        /// Gets or sets whether this command should be tracked in analytics.
+        /// </summary>
+        public bool AnalyticsTrack { get; set; }
 
 
         /// <summary>
         /// <summary>
         ///     Gets or sets the path to the menu item. If null, command will not be added to menu.
         ///     Gets or sets the path to the menu item. If null, command will not be added to menu.

+ 10 - 0
src/PixiEditor/Models/Commands/CommandContext/CommandExecutionContext.cs

@@ -0,0 +1,10 @@
+using System.Text.Json;
+
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public class CommandExecutionContext(object parameter, ICommandExecutionSourceInfo sourceInfo)
+{
+    public object Parameter { get; set; } = parameter;
+
+    public ICommandExecutionSourceInfo SourceInfo { get; set; } = sourceInfo;
+}

+ 8 - 0
src/PixiEditor/Models/Commands/CommandContext/CommandExecutionSourceType.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public enum CommandExecutionSourceType
+{
+    Unknown,
+    Shortcut,
+    Search
+}

+ 6 - 0
src/PixiEditor/Models/Commands/CommandContext/ICommandExecutionSourceInfo.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public interface ICommandExecutionSourceInfo
+{
+    public CommandExecutionSourceType SourceType { get; }
+}

+ 13 - 0
src/PixiEditor/Models/Commands/CommandContext/SearchSourceInfo.cs

@@ -0,0 +1,13 @@
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public class SearchSourceInfo(string searchTerm, int index) : ICommandExecutionSourceInfo
+{
+    public CommandExecutionSourceType SourceType { get; } = CommandExecutionSourceType.Search;
+
+    public string SearchTerm { get; set; } = searchTerm;
+
+    public int Index { get; set; } = index;
+
+    public static CommandExecutionContext GetContext(object parameter, string searchTerm, int index) =>
+        new(parameter, new SearchSourceInfo(searchTerm, index));
+}

+ 13 - 0
src/PixiEditor/Models/Commands/CommandContext/ShortcutSourceInfo.cs

@@ -0,0 +1,13 @@
+using PixiEditor.Models.Input;
+
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public class ShortcutSourceInfo(KeyCombination combination) : ICommandExecutionSourceInfo
+{
+    public CommandExecutionSourceType SourceType { get; } = CommandExecutionSourceType.Shortcut;
+    
+    public KeyCombination Shortcut { get; }
+
+    public static CommandExecutionContext GetContext(object parameter, KeyCombination shortcut) =>
+        new(parameter, new ShortcutSourceInfo(shortcut));
+}

+ 57 - 28
src/PixiEditor/Models/Commands/CommandController.cs

@@ -8,7 +8,9 @@ using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.Attributes.Evaluators;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
@@ -330,33 +332,6 @@ internal class CommandController
 
 
             var parameters = method?.GetParameters();
             var parameters = method?.GetParameters();
 
 
-            async void ActionOnException(Task faultedTask)
-            {
-                // since this method is "async void" and not "async Task", the runtime will propagate exceptions out if it
-                // (instead of putting them into the returned task and forgetting about them)
-                await faultedTask; // this instantly throws the exception from the already faulted task
-            }
-
-            Action<object> action;
-            if (parameters is not { Length: 1 })
-            {
-                action = x =>
-                {
-                    object result = method.Invoke(instance, null);
-                    if (result is Task task)
-                        task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
-                };
-            }
-            else
-            {
-                action = x =>
-                {
-                    object result = method.Invoke(instance, new[] { x });
-                    if (result is Task task)
-                        task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
-                };
-            }
-
             string name = attribute.InternalName;
             string name = attribute.InternalName;
             bool isDebug = attribute.InternalName.StartsWith("#DEBUG#");
             bool isDebug = attribute.InternalName.StartsWith("#DEBUG#");
 
 
@@ -368,7 +343,7 @@ internal class CommandController
             var command = commandFactory(
             var command = commandFactory(
                 isDebug,
                 isDebug,
                 name,
                 name,
-                action,
+                CommandAction,
                 attribute.CanExecute != null ? CanExecuteEvaluators[attribute.CanExecute] : CanExecuteEvaluator.AlwaysTrue,
                 attribute.CanExecute != null ? CanExecuteEvaluators[attribute.CanExecute] : CanExecuteEvaluator.AlwaysTrue,
                 attribute.IconEvaluator != null ? IconEvaluators[attribute.IconEvaluator] : IconEvaluator.Default);
                 attribute.IconEvaluator != null ? IconEvaluators[attribute.IconEvaluator] : IconEvaluator.Default);
 
 
@@ -376,6 +351,60 @@ internal class CommandController
             AddCommandToCommandsCollection(command, commandGroupsData, commands);
             AddCommandToCommandsCollection(command, commandGroupsData, commands);
 
 
             return command;
             return command;
+
+            void CommandAction(object x) => CommandMethodInvoker(method, name, instance, x, parameters, attribute.AnalyticsTrack);
+
+            
+        }
+    }
+
+    private static void CommandMethodInvoker(MethodInfo method, string name, object? instance, object parameter, ParameterInfo[] parameterInfos, bool isTracking)
+    {
+        var parameters = GetParameters(parameter, parameterInfos);
+                
+        if (isTracking)
+        {
+            Analytics.SendCommand(name, (parameter as CommandExecutionContext)?.SourceInfo);
+        }
+                
+        object result = method.Invoke(instance, parameters);
+        if (result is Task task)
+            task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+
+        return;
+
+        static async void ActionOnException(Task faultedTask)
+        {
+            // since this method is "async void" and not "async Task", the runtime will propagate exceptions out if it
+            // (instead of putting them into the returned task and forgetting about them)
+            await faultedTask; // this instantly throws the exception from the already faulted task
+        }
+
+        static object?[]? GetParameters(object parameter, ParameterInfo[] parameterInfos)
+        {
+            object?[]? parameters;
+
+            if (parameterInfos.Length == 0)
+            {
+                parameters = null;
+            }
+            else if (parameter is CommandExecutionContext context)
+            {
+                if (parameterInfos[0].ParameterType == typeof(CommandExecutionContext))
+                {
+                    parameters = [ context ];
+                }
+                else
+                {
+                    parameters = [ context.Parameter ];
+                }
+            }
+            else
+            {
+                parameters = [ parameter ];
+            }
+
+            return parameters;
         }
         }
     }
     }
 
 

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

@@ -1,6 +1,7 @@
 using System.Diagnostics;
 using System.Diagnostics;
 using Avalonia.Media;
 using Avalonia.Media;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Input;
 using PixiEditor.Models.Input;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
@@ -70,6 +71,16 @@ internal abstract partial class Command : PixiObservableObject
 
 
     public void Execute() => Methods.Execute(GetParameter());
     public void Execute() => Methods.Execute(GetParameter());
 
 
+    public void Execute(CommandExecutionContext context, bool keepParameter)
+    {
+        if (!keepParameter)
+        {
+            context.Parameter = GetParameter();
+        }
+        
+        Methods.Execute(context);
+    }
+
     public bool CanExecute() => Methods.CanExecute(GetParameter());
     public bool CanExecute() => Methods.CanExecute(GetParameter());
 
 
     public IImage GetIcon() => IconEvaluator == null ? null : IconEvaluator.CallEvaluate(this, GetParameter());
     public IImage GetIcon() => IconEvaluator == null ? null : IconEvaluator.CallEvaluate(this, GetParameter());