Browse Source

Added command tracking

CPKreuz 1 year ago
parent
commit
da03cfb6d2

+ 19 - 2
src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs

@@ -1,12 +1,16 @@
 using System.Net;
 using System.Net.Http.Json;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Input;
 
 namespace PixiEditor.Models.AnalyticsAPI;
 
 public class AnalyticsClient
 {
     private readonly HttpClient _client = new();
+    private readonly JsonSerializerOptions _options = new() { Converters = { new JsonStringEnumConverter(), new KeyCombinationConverter() } };
 
     public AnalyticsClient(string url)
     {
@@ -19,7 +23,7 @@ public class AnalyticsClient
 
         if (response.IsSuccessStatusCode)
         {
-            return await response.Content.ReadFromJsonAsync<Guid?>(cancellationToken);
+            return await response.Content.ReadFromJsonAsync<Guid?>(_options, cancellationToken);
         }
 
         if (response.StatusCode is not HttpStatusCode.ServiceUnavailable)
@@ -34,7 +38,7 @@ public class AnalyticsClient
     public async Task<string?> SendEventsAsync(Guid sessionId, IEnumerable<AnalyticEvent> events,
         CancellationToken cancellationToken = default)
     {
-        var response = await _client.PostAsJsonAsync($"post-events?id={sessionId}", events, cancellationToken);
+        var response = await _client.PostAsJsonAsync($"post-events?id={sessionId}", events, _options, cancellationToken);
         
         if (response.IsSuccessStatusCode)
         {
@@ -65,4 +69,17 @@ public class AnalyticsClient
     {
         await CrashHelper.SendExceptionInfoToWebhookAsync(new InvalidOperationException($"Invalid status code from analytics API '{statusCode}'"));
     }
+
+    class KeyCombinationConverter : JsonConverter<KeyCombination>
+    {
+        public override KeyCombination Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override void Write(Utf8JsonWriter writer, KeyCombination value, JsonSerializerOptions options)
+        {
+            writer.WriteStringValue(value.ToString());
+        }
+    }
 }

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

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public class CommandBindingSourceInfo(string tag) : ICommandExecutionSourceInfo
+{
+    public CommandExecutionSourceType SourceType => CommandExecutionSourceType.CommandBinding;
+
+    public string Tag { get; } = tag;
+}

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

@@ -4,5 +4,7 @@ public enum CommandExecutionSourceType
 {
     Unknown,
     Shortcut,
+    Menu,
+    CommandBinding,
     Search
 }

+ 7 - 1
src/PixiEditor/Models/Commands/CommandContext/ICommandExecutionSourceInfo.cs

@@ -1,5 +1,11 @@
-namespace PixiEditor.Models.Commands.CommandContext;
+using System.Text.Json.Serialization;
 
+namespace PixiEditor.Models.Commands.CommandContext;
+
+[JsonDerivedType(typeof(ShortcutSourceInfo))]
+[JsonDerivedType(typeof(MenuSourceInfo))]
+[JsonDerivedType(typeof(CommandBindingSourceInfo))]
+[JsonDerivedType(typeof(SearchSourceInfo))]
 public interface ICommandExecutionSourceInfo
 {
     public CommandExecutionSourceType SourceType { get; }

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

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public class MenuSourceInfo(MenuType menuType) : ICommandExecutionSourceInfo
+{
+    public CommandExecutionSourceType SourceType { get; } = CommandExecutionSourceType.Menu;
+
+    public MenuType MenuType { get; } = menuType;
+}

+ 7 - 0
src/PixiEditor/Models/Commands/CommandContext/MenuType.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Models.Commands.CommandContext;
+
+public enum MenuType
+{
+    Menu,
+    ContextMenu
+}

+ 4 - 6
src/PixiEditor/Models/Commands/CommandContext/SearchSourceInfo.cs

@@ -1,13 +1,11 @@
 namespace PixiEditor.Models.Commands.CommandContext;
 
-public class SearchSourceInfo(string searchTerm, int index) : ICommandExecutionSourceInfo
+public class SearchSourceInfo(string searchTerm) : ICommandExecutionSourceInfo
 {
-    public CommandExecutionSourceType SourceType { get; } = CommandExecutionSourceType.Search;
+    public CommandExecutionSourceType SourceType => 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));
+    public static CommandExecutionContext GetContext(string searchTerm) =>
+        new(null, new SearchSourceInfo(searchTerm));
 }

+ 6 - 6
src/PixiEditor/Models/Commands/CommandContext/ShortcutSourceInfo.cs

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

+ 2 - 2
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -71,9 +71,9 @@ internal abstract partial class Command : PixiObservableObject
 
     public void Execute() => Methods.Execute(GetParameter());
 
-    public void Execute(CommandExecutionContext context, bool keepParameter)
+    public void Execute(CommandExecutionContext context, bool useContextParameter)
     {
-        if (!keepParameter)
+        if (!useContextParameter)
         {
             context.Parameter = GetParameter();
         }

+ 2 - 1
src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs

@@ -1,4 +1,5 @@
 using Avalonia.Media;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Input;
 
@@ -20,6 +21,6 @@ internal class CommandSearchResult : SearchResult
 
     public override void Execute()
     {
-        Command.Execute();
+        Command.Execute(SearchSourceInfo.GetContext(SearchTerm), false);
     }
 }

+ 2 - 0
src/PixiEditor/Models/Commands/Search/SearchResult.cs

@@ -19,6 +19,8 @@ internal abstract class SearchResult : ObservableObject
     private bool isMouseSelected;
 
     public string SearchTerm { get; init; }
+    
+    public int Index { get; init; }
 
     public virtual Inline[] TextBlockContent => GetInlines().ToArray();
 

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

@@ -3,6 +3,7 @@ using System.Windows.Input;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Commands.CommandContext;
 
 namespace PixiEditor.Models.Commands.XAML;
 
@@ -15,6 +16,8 @@ internal class Command : MarkupExtension
     public bool UseProvided { get; set; }
 
     public bool GetPixiCommand { get; set; }
+    
+    public string SourceInfoTag { get; set; }
 
     public Command() { }
 
@@ -33,7 +36,7 @@ internal class Command : MarkupExtension
                     Description = attribute.Description,
                     DefaultShortcut = attribute.GetShortcut(),
                     Shortcut = attribute.GetShortcut()
-                }, false);
+                }, null, false);
         }
 
         if (commandController is null)
@@ -42,13 +45,14 @@ internal class Command : MarkupExtension
         }
 
         Commands.Command command = commandController.Commands[Name];
-        return GetPixiCommand ? command : GetICommand(command, UseProvided);
+        return GetPixiCommand ? command : GetICommand(command, new CommandBindingSourceInfo(SourceInfoTag), UseProvided);
     }
 
-    public static ICommand GetICommand(Commands.Command command, bool useProvidedParameter) => new ProvidedICommand()
+    public static ICommand GetICommand(Commands.Command command, ICommandExecutionSourceInfo? source, bool useProvidedParameter) => new ProvidedICommand()
     {
         Command = command,
         UseProvidedParameter = useProvidedParameter,
+        Source = source
     };
 
     class ProvidedICommand : ICommand
@@ -90,18 +94,14 @@ internal class Command : MarkupExtension
 
         public bool UseProvidedParameter { get; init; }
 
+        public ICommandExecutionSourceInfo Source { get; init; }
+        
         public bool CanExecute(object parameter) => UseProvidedParameter ? Command.Methods.CanExecute(parameter) : Command.CanExecute();
 
         public void Execute(object parameter)
         {
-            if (UseProvidedParameter)
-            {
-                Command.Methods.Execute(parameter);
-            }
-            else
-            {
-                Command.Execute();
-            }
+            var context = new CommandExecutionContext(parameter, Source);
+            Command.Execute(context, UseProvidedParameter);
         }
     }
 }

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

@@ -1,6 +1,7 @@
 using Avalonia;
 using Avalonia.Controls;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Input;
 
 namespace PixiEditor.Models.Commands.XAML;
@@ -34,7 +35,7 @@ internal class ContextMenu : global::Avalonia.Controls.ContextMenu
 
         var command = CommandController.Current.Commands[value];
 
-        item.Command = Command.GetICommand(command, false);
+        item.Command = Command.GetICommand(command, new MenuSourceInfo(MenuType.ContextMenu), false);
         item.Bind(MenuItem.InputGestureProperty, ShortcutBinding.GetBinding(command, null, true));
     }
 

+ 2 - 1
src/PixiEditor/Models/Commands/XAML/Menu.cs

@@ -4,6 +4,7 @@ using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Input;
 
 namespace PixiEditor.Models.Commands.XAML;
@@ -49,7 +50,7 @@ internal class Menu : global::Avalonia.Controls.Menu
             HorizontalAlignment = HorizontalAlignment.Center,
         };
 
-        item.Command = Command.GetICommand(command, false);
+        item.Command = Command.GetICommand(command, new MenuSourceInfo(MenuType.Menu), false);
         item.Icon = icon;
         item.Bind(MenuItem.InputGestureProperty, ShortcutBinding.GetBinding(command, null, true));
     }

+ 15 - 11
src/PixiEditor/Models/Controllers/ShortcutController.cs

@@ -2,6 +2,7 @@
 using System.Linq;
 using Avalonia.Input;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Input;
 using PixiEditor.ViewModels.Tools;
@@ -51,21 +52,24 @@ internal class ShortcutController
     {
         KeyCombination shortcut = new(key, modifiers);
 
-        if (!ShortcutExecutionBlocked)
+        if (ShortcutExecutionBlocked)
         {
-            var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContext is null || x.ShortcutContext == ActiveContext).ToList();
+            return;
+        }
 
-            if (!commands.Any())
-            {
-                return;
-            }
+        var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContext is null || x.ShortcutContext == ActiveContext).ToList();
 
-            LastCommands = commands;
+        if (!commands.Any())
+        {
+            return;
+        }
 
-            foreach (var command in commands)
-            {
-                command.Execute();
-            }
+        LastCommands = commands;
+
+        var context = ShortcutSourceInfo.GetContext(shortcut);
+        foreach (var command in commands)
+        {
+            command.Execute(context, false);
         }
     }
 

+ 2 - 1
src/PixiEditor/Models/Services/CommandProvider.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using Avalonia.Media;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.CommandContext;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using XAMLCommand = PixiEditor.Models.Commands.XAML.Command;
@@ -28,5 +29,5 @@ internal class CommandProvider
     public IImage GetIcon(string name, Command command, object argument) =>
         _controller.IconEvaluators[name].CallEvaluate(command, argument);
 
-    public ICommand GetICommand(string name, bool useProvidedArgument = false) => Commands.XAML.Command.GetICommand(_controller.Commands[name], useProvidedArgument);
+    public ICommand GetICommand(string name, ICommandExecutionSourceInfo source, bool useProvidedArgument = false) => Commands.XAML.Command.GetICommand(_controller.Commands[name], source, useProvidedArgument);
 }