Prechádzať zdrojové kódy

Fix replaced shortcut not getting saved properly, refactor

Equbuxu 3 rokov pred
rodič
commit
875712ab4c
27 zmenil súbory, kde vykonal 249 pridanie a 364 odobranie
  1. 1 1
      PixiEditor/Helpers/DesignCommandHelpers.cs
  2. 1 1
      PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  3. 17 12
      PixiEditor/Helpers/GlobalMouseHook.cs
  4. 9 9
      PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs
  5. 6 6
      PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  6. 5 5
      PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs
  7. 3 3
      PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs
  8. 30 14
      PixiEditor/Models/Commands/CommandCollection.cs
  9. 130 115
      PixiEditor/Models/Commands/CommandController.cs
  10. 4 4
      PixiEditor/Models/Commands/CommandGroup.cs
  11. 1 1
      PixiEditor/Models/Commands/CommandMethods.cs
  12. 3 3
      PixiEditor/Models/Commands/Commands/Command.cs
  13. 1 1
      PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs
  14. 5 2
      PixiEditor/Models/Commands/Evaluators/Evaluator.cs
  15. 3 3
      PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs
  16. 1 1
      PixiEditor/Models/Commands/Search/CommandSearchResult.cs
  17. 2 2
      PixiEditor/Models/Commands/ShortcutFile.cs
  18. 1 1
      PixiEditor/Models/Commands/XAML/Command.cs
  19. 0 150
      PixiEditor/Models/DataHolders/EnumerableDictionary.cs
  20. 2 2
      PixiEditor/Models/Services/CommandProvider.cs
  21. 1 2
      PixiEditor/ViewModels/SettingsWindowViewModel.cs
  22. 1 1
      PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs
  23. 1 1
      PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  24. 2 2
      PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs
  25. 1 0
      PixiEditor/Views/Dialogs/OptionsPopup.xaml
  26. 0 3
      PixiEditor/Views/Dialogs/OptionsPopup.xaml.cs
  27. 18 19
      PixiEditor/Views/UserControls/ShortcutBox.cs

+ 1 - 1
PixiEditor/Helpers/DesignCommandHelpers.cs

@@ -23,7 +23,7 @@ namespace PixiEditor.Helpers
                     .SelectMany(x => x.GetCustomAttributes<CommandAttribute.CommandAttribute>());
             }
 
-            var command = _commands.SingleOrDefault(x => x.Name == name || x.Name == $"#DEBUG#{name}");
+            var command = _commands.SingleOrDefault(x => x.InternalName == name || x.InternalName == $"#DEBUG#{name}");
 
             if (command == null)
             {

+ 1 - 1
PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -33,7 +33,7 @@ namespace PixiEditor.Helpers.Extensions
                 .AddSingleton<ColorsViewModel>()
                 .AddSingleton<DocumentViewModel>()
                 .AddSingleton<MiscViewModel>()
-                .AddSingleton(x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
+                .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
                 .AddSingleton<DebugViewModel>()
                 .AddSingleton<SearchViewModel>()
                 // Controllers

+ 17 - 12
PixiEditor/Helpers/GlobalMouseHook.cs

@@ -5,8 +5,8 @@ using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Input;
-using System.Windows.Threading;
-
+using System.Windows.Threading;
+
 namespace PixiEditor.Helpers
 {
     public delegate void MouseUpEventHandler(object sender, Point p, MouseButton button);
@@ -14,7 +14,7 @@ namespace PixiEditor.Helpers
     // see https://stackoverflow.com/questions/22659925/how-to-capture-mouseup-event-outside-the-wpf-window
     [ExcludeFromCodeCoverage]
     public static class GlobalMouseHook
-    {
+    {
         private const int WH_MOUSE_LL = 14;
         private const int WM_LBUTTONUP = 0x0202;
         private const int WM_MBUTTONUP = 0x0208;
@@ -23,20 +23,25 @@ namespace PixiEditor.Helpers
         private static int mouseHookHandle;
         private static HookProc mouseDelegate;
 
-        private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
-
+        private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
+
         public static event MouseUpEventHandler OnMouseUp
         {
             add
-            {
+            { 
+// disable low-level hook in debug to prevent mouse lag when pausing in debugger
+#if !DEBUG
                 Subscribe();
+#endif
                 MouseUp += value;
             }
-
+
             remove
             {
                 MouseUp -= value;
+#if !DEBUG
                 Unsubscribe();
+#endif
             }
         }
 
@@ -91,13 +96,13 @@ namespace PixiEditor.Helpers
                     {
 
                         MouseButton button = wParam == WM_LBUTTONUP ? MouseButton.Left
-                            : wParam == WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
-                        Dispatcher.CurrentDispatcher.BeginInvoke(() =>
+                            : wParam == WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
+                        Dispatcher.CurrentDispatcher.BeginInvoke(() =>
                             MouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button));
                     }
                 }
             }
-
+
             return CallNextHookEx(mouseHookHandle, nCode, wParam, lParam);
         }
 
@@ -122,8 +127,8 @@ namespace PixiEditor.Helpers
         private static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);
 
         [DllImport("kernel32.dll")]
-        private static extern IntPtr GetModuleHandle(string name);
-
+        private static extern IntPtr GetModuleHandle(string name);
+
         [StructLayout(LayoutKind.Sequential)]
         private struct POINT
         {

+ 9 - 9
PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs

@@ -12,25 +12,25 @@ public partial class Command
         /// <summary>
         /// Create's a basic command which uses null as a paramter
         /// </summary>
-        /// <param name="name">The name of the command</param>
-        /// <param name="display">A short description which is displayed in the the top menu, e.g. "Save as..."</param>
+        /// <param name="internalName">The internal name of the command</param>
+        /// <param name="displayName">A short description which is displayed in the the top menu, e.g. "Save as..."</param>
         /// <param name="description">A description which is displayed in the search bar, e.g. "Save image as new". Leave empty to hide it from the search bar</param>
-        public BasicAttribute(string name, string display, string description)
-            : this(name, null, display, description)
+        public BasicAttribute(string internalName, string displayName, string description)
+            : this(internalName, null, displayName, description)
         {
         }
 
         /// <summary>
         /// Create's a basic command which uses <paramref name="parameter"/> as the parameter
         /// </summary>
-        /// <param name="name">The name of the command</param>
+        /// <param name="internalName">The internal name of the command</param>
         /// <param name="parameter">The parameter that will be passed to the first argument of the method</param>
-        /// <param name="display">A short description which is displayed in the the top menu, e.g. "Save as..."</param>
+        /// <param name="displayName">A short description which is displayed in the the top menu, e.g. "Save as..."</param>
         /// <param name="description">A description which is displayed in the search bar, e.g. "Save image as new". Leave empty to hide it from the search bar</param>
-        public BasicAttribute(string name, object parameter, string display, string description)
-            : base(name, display, description)
+        public BasicAttribute(string internalName, object parameter, string displayName, string description)
+            : base(internalName, displayName, description)
         {
             Parameter = parameter;
         }
     }
-}
+}

+ 6 - 6
PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -8,9 +8,9 @@ public partial class Command
     [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
     public abstract class CommandAttribute : Attribute
     {
-        public string Name { get; }
+        public string InternalName { get; }
 
-        public string Display { get; }
+        public string DisplayName { get; }
 
         public string Description { get; }
 
@@ -34,12 +34,12 @@ public partial class Command
         /// <summary>
         /// Gets or sets path to the icon. Must be bitmap image
         /// </summary>
-        public string Icon { get; set; }
+        public string IconPath { get; set; }
 
-        protected CommandAttribute(string name, string display, string description)
+        protected CommandAttribute(string internalName, string displayName, string description)
         {
-            Name = name;
-            Display = display;
+            InternalName = internalName;
+            DisplayName = displayName;
             Description = description;
         }
 

+ 5 - 5
PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs

@@ -5,17 +5,17 @@
         [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
         public class GroupAttribute : Attribute
         {
-            public string Name { get; }
+            public string InternalName { get; }
 
-            public string Display { get; }
+            public string DisplayName { get; }
 
             /// <summary>
-            /// Group's all commands that start with the name <paramref name="name"/>
+            /// Groups all commands that start with the name <paramref name="name"/>
             /// </summary>
             public GroupAttribute(string name, string display)
             {
-                Name = name;
-                Display = display;
+                InternalName = name;
+                DisplayName = display;
             }
         }
     }

+ 3 - 3
PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs

@@ -5,16 +5,16 @@ public partial class Evaluator
     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
     public class CanExecuteAttribute : EvaluatorAttribute
     {
-        public string[] Requires { get; }
+        public string[] NamesOfRequiredCanExecuteEvaluators { get; }
 
         public CanExecuteAttribute(string name) : base(name)
         {
-            Requires = Array.Empty<string>();
+            NamesOfRequiredCanExecuteEvaluators = Array.Empty<string>();
         }
 
         public CanExecuteAttribute(string name, params string[] requires) : base(name)
         {
-            Requires = requires;
+            NamesOfRequiredCanExecuteEvaluators = requires;
         }
     }
 }

+ 30 - 14
PixiEditor/Models/Commands/CommandCollection.cs

@@ -1,64 +1,80 @@
 using PixiEditor.Models.DataHolders;
 using System.Collections;
 using System.Diagnostics;
+using System.Windows.Input;
+using OneOf.Types;
 
 namespace PixiEditor.Models.Commands
 {
     [DebuggerDisplay("Count = {Count}")]
     public class CommandCollection : ICollection<Command>
     {
-        private readonly Dictionary<string, Command> _commandNames;
-        private readonly EnumerableDictionary<KeyCombination, Command> _commandShortcuts;
+        private readonly Dictionary<string, Command> _commandInternalNames;
+        private readonly OneToManyDictionary<KeyCombination, Command> _commandShortcuts;
 
-        public int Count => _commandNames.Count;
+        public int Count => _commandInternalNames.Count;
 
         public bool IsReadOnly => false;
 
-        public Command this[string name] => _commandNames[name];
+        public Command this[string name] => _commandInternalNames[name];
 
         public IEnumerable<Command> this[KeyCombination shortcut] => _commandShortcuts[shortcut];
 
         public CommandCollection()
         {
-            _commandNames = new();
+            _commandInternalNames = new();
             _commandShortcuts = new();
         }
 
         public void Add(Command item)
         {
-            _commandNames.Add(item.Name, item);
+            _commandInternalNames.Add(item.Name, item);
             _commandShortcuts.Add(item.Shortcut, item);
         }
 
         public void Clear()
         {
-            _commandNames.Clear();
+            _commandInternalNames.Clear();
             _commandShortcuts.Clear();
         }
 
         public void ClearShortcuts() => _commandShortcuts.Clear();
 
-        public bool Contains(Command item) => _commandNames.ContainsKey(item.Name);
+        public bool Contains(Command item) => _commandInternalNames.ContainsKey(item.Name);
 
-        public void CopyTo(Command[] array, int arrayIndex) => _commandNames.Values.CopyTo(array, arrayIndex);
+        public void CopyTo(Command[] array, int arrayIndex) => _commandInternalNames.Values.CopyTo(array, arrayIndex);
 
-        public IEnumerator<Command> GetEnumerator() => _commandNames.Values.GetEnumerator();
+        public IEnumerator<Command> GetEnumerator() => _commandInternalNames.Values.GetEnumerator();
 
         public bool Remove(Command item)
         {
             bool anyRemoved = false;
 
-            anyRemoved |= _commandNames.Remove(item.Name);
+            anyRemoved |= _commandInternalNames.Remove(item.Name);
             anyRemoved |= _commandShortcuts.Remove(item);
 
             return anyRemoved;
         }
 
-        public void AddShortcut(Command command, KeyCombination shortcut) => _commandShortcuts.Add(shortcut, command);
+        public void AddShortcut(Command command, KeyCombination shortcut)
+        {
+            _commandShortcuts.Remove(KeyCombination.None, command);
+            _commandShortcuts.Add(shortcut, command);
+        }
 
-        public void RemoveShortcut(Command command, KeyCombination shortcut) => _commandShortcuts.Remove(shortcut, command);
+        public void RemoveShortcut(Command command, KeyCombination shortcut)
+        {
+            _commandShortcuts.Remove(shortcut, command);
+            _commandShortcuts.Add(KeyCombination.None, command);
+        }
 
-        public void ClearShortcut(KeyCombination shortcut) => _commandShortcuts.Clear(shortcut);
+        public void ClearShortcut(KeyCombination shortcut)
+        {
+            if (shortcut is { Key: Key.None, Modifiers: ModifierKeys.None })
+                return;
+            _commandShortcuts.AddRange(KeyCombination.None, _commandShortcuts[shortcut]);
+            _commandShortcuts.Clear(shortcut);      
+        }
 
         public IEnumerable<KeyValuePair<KeyCombination, IEnumerable<Command>>> GetShortcuts() =>
             _commandShortcuts;

+ 130 - 115
PixiEditor/Models/Commands/CommandController.cs

@@ -5,7 +5,6 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Tools;
 using System.IO;
 using System.Reflection;
-using System.Windows.Input;
 using System.Windows.Media;
 using CommandAttribute = PixiEditor.Models.Commands.Attributes.Command;
 
@@ -42,137 +41,158 @@ namespace PixiEditor.Models.Commands
             IconEvaluators = new();
         }
 
-        public void Init(IServiceProvider services)
+        private static List<(string internalName, string displayName)> FindCommandGroups(Type[] typesToSearchForAttributes)
         {
-            KeyValuePair<KeyCombination, IEnumerable<string>>[] shortcuts = shortcutFile.GetShortcuts()?.ToArray()
-                ?? Array.Empty<KeyValuePair<KeyCombination, IEnumerable<string>>>();
-
-            var types = typeof(CommandController).Assembly.GetTypes();
+            List<(string internalName, string displayName)> result = new();
 
-            EnumerableDictionary<string, string> groups = new();
-            EnumerableDictionary<string, Command> commandGroups = new();
-
-            foreach (var type in types)
+            foreach (var type in typesToSearchForAttributes)
             {
-                object instanceType = null;
-
                 foreach (var group in type.GetCustomAttributes<CommandAttribute.GroupAttribute>())
                 {
-                    groups.Add(group.Display, group.Name);
+                    result.Add((group.InternalName, group.DisplayName));
                 }
+            }
 
-                var methods = type.GetMethods();
+            return result;
+        }
 
+        private static void ForEachMethod
+            (Type[] typesToSearchForMethods, IServiceProvider serviceProvider, Action<MethodInfo, object> action)
+        {
+            foreach (var type in typesToSearchForMethods)
+            {
+                object serviceInstance = serviceProvider.GetService(type);
+                var methods = type.GetMethods();
                 foreach (var method in methods)
                 {
-                    var evaluatorAttrs = method.GetCustomAttributes<Evaluator.EvaluatorAttribute>();
-
-                    if (instanceType is null && evaluatorAttrs.Any())
-                    {
-                        instanceType = services.GetService(type);
-                    }
-
-                    foreach (var attribute in evaluatorAttrs)
-                    {
-                        if (attribute is Evaluator.CanExecuteAttribute canExecute)
-                        {
-                            var required = (CommandController controller) => canExecute.Requires.Select(x => controller.CanExecuteEvaluators[x]);
-                            AddEvaluatorFactory<Evaluator.CanExecuteAttribute, CanExecuteEvaluator, bool>(
-                                method,
-                                instanceType,
-                                canExecute,
-                                CanExecuteEvaluators,
-                                x => new CanExecuteEvaluator()
-                                {
-                                    Name = attribute.Name, 
-                                    Evaluate = y => x.Invoke(y) && required.Invoke(this).All(z => z.EvaluateEvaluator(null, y))
-                                });
-                        }
-                        else if (attribute is Evaluator.IconAttribute icon)
-                        {
-                            AddEvaluator<Evaluator.IconAttribute, IconEvaluator, ImageSource>(method, instanceType, icon, IconEvaluators);
-                        }
-                    }
+                    action(method, serviceInstance);
                 }
             }
+        }
 
-            foreach (var type in types)
-            {
-                object instanceType = null;
-                var methods = type.GetMethods();
+        public void Init(IServiceProvider serviceProvider)
+        {
+            KeyValuePair<KeyCombination, IEnumerable<string>>[] shortcuts = shortcutFile.LoadShortcuts()?.ToArray()
+                ?? Array.Empty<KeyValuePair<KeyCombination, IEnumerable<string>>>();
 
-                foreach (var method in methods)
-                {
-                    var commandAttrs = method.GetCustomAttributes<CommandAttribute.CommandAttribute>();
+            Type[] allTypesInPixiEditorAssembly = typeof(CommandController).Assembly.GetTypes();
 
-                    if (instanceType is null && commandAttrs.Any())
-                    {
-                        instanceType = services.GetService(type);
-                    }
+            List<(string internalName, string displayName)> commandGroupsData = FindCommandGroups(allTypesInPixiEditorAssembly);
+            OneToManyDictionary<string, Command> commands = new(); // internal name of the corr. group -> command in that group
 
-                    foreach (var attribute in commandAttrs)
+            // Find evaluators
+            ForEachMethod(allTypesInPixiEditorAssembly, serviceProvider, (methodInfo, maybeServiceInstance) =>
+            {
+                var evaluatorAttrs = methodInfo.GetCustomAttributes<Evaluator.EvaluatorAttribute>();
+                foreach (var attribute in evaluatorAttrs)
+                {
+                    switch (attribute)
                     {
-                        if (attribute is CommandAttribute.BasicAttribute basic)
-                        {
-                            AddCommand(method, instanceType, attribute, (isDebug, name, x, xCan, xIcon) => new Command.BasicCommand(x, xCan)
+                        case Evaluator.CanExecuteAttribute canExecuteAttribute:
                             {
-                                Name = name,
-                                IsDebug = isDebug,
-                                Display = attribute.Display,
-                                Description = attribute.Description,
-                                IconPath = attribute.Icon,
-                                IconEvaluator = xIcon,
-                                DefaultShortcut = attribute.GetShortcut(),
-                                Shortcut = GetShortcut(name, attribute.GetShortcut()),
-                                Parameter = basic.Parameter,
-                            });
-                        }
+                                var getRequiredEvaluatorsObjectsOfCurrentEvaluator =
+                                    (CommandController controller) =>
+                                        canExecuteAttribute.NamesOfRequiredCanExecuteEvaluators.Select(x => controller.CanExecuteEvaluators[x]);
+
+                                AddEvaluatorFactory<Evaluator.CanExecuteAttribute, CanExecuteEvaluator, bool>(
+                                    methodInfo,
+                                    maybeServiceInstance,
+                                    canExecuteAttribute,
+                                    CanExecuteEvaluators,
+                                    evaluateFunction => new CanExecuteEvaluator()
+                                    {
+                                        Name = attribute.Name,
+                                        Evaluate = evaluateFunctionArgument =>
+                                            evaluateFunction.Invoke(evaluateFunctionArgument) &&
+                                            getRequiredEvaluatorsObjectsOfCurrentEvaluator.Invoke(this).All(requiredEvaluator =>
+                                                requiredEvaluator.CallEvaluate(null, evaluateFunctionArgument))
+                                    });
+                                break;
+                            }
+                        case Evaluator.IconAttribute icon:
+                            AddEvaluator<Evaluator.IconAttribute, IconEvaluator, ImageSource>(methodInfo, maybeServiceInstance, icon, IconEvaluators);
+                            break;
                     }
                 }
+            });
 
-                if (type.IsAssignableTo(typeof(Tool)))
-                {
-                    var toolAttr = type.GetCustomAttribute<CommandAttribute.ToolAttribute>();
+            // Find basic commands
+            ForEachMethod(allTypesInPixiEditorAssembly, serviceProvider, (methodInfo, maybeServiceInstance) =>
+            {
+                var commandAttrs = methodInfo.GetCustomAttributes<CommandAttribute.CommandAttribute>();
 
-                    if (toolAttr != null)
+                foreach (var attribute in commandAttrs)
+                {
+                    if (attribute is CommandAttribute.BasicAttribute basic)
                     {
-                        var tool = services.GetServices<Tool>().First(x => x.GetType() == type);
-                        string name = $"PixiEditor.Tools.Select.{type.Name}";
-
-                        var command = new Command.ToolCommand()
+                        AddCommand(methodInfo, maybeServiceInstance, attribute, (isDebug, name, x, xCan, xIcon) => new Command.BasicCommand(x, xCan)
                         {
                             Name = name,
-                            Display = $"Select {tool.DisplayName} Tool",
-                            Description = $"Select {tool.DisplayName} Tool",
-                            IconPath = $"@{tool.ImagePath}",
-                            IconEvaluator = IconEvaluator.Default,
-                            TransientKey = toolAttr.Transient,
-                            DefaultShortcut = toolAttr.GetShortcut(),
-                            Shortcut = GetShortcut(name, toolAttr.GetShortcut()),
-                            ToolType = type,
-                        };
-
-                        Commands.Add(command);
-                        AddCommandToGroup(command);
+                            IsDebug = isDebug,
+                            Display = attribute.DisplayName,
+                            Description = attribute.Description,
+                            IconPath = attribute.IconPath,
+                            IconEvaluator = xIcon,
+                            DefaultShortcut = attribute.GetShortcut(),
+                            Shortcut = GetShortcut(name, attribute.GetShortcut()),
+                            Parameter = basic.Parameter,
+                        });
                     }
                 }
+            });
+
+            // Find tool commands
+            foreach (var type in allTypesInPixiEditorAssembly)
+            {
+                if (!type.IsAssignableTo(typeof(Tool)))
+                    continue;
+
+                var toolAttr = type.GetCustomAttribute<CommandAttribute.ToolAttribute>();
+                if (toolAttr is null)
+                    continue;
+
+                Tool toolInstance = serviceProvider.GetServices<Tool>().First(x => x.GetType() == type);
+                string internalName = $"PixiEditor.Tools.Select.{type.Name}";
+
+                var command = new Command.ToolCommand()
+                {
+                    Name = internalName,
+                    Display = $"Select {toolInstance.DisplayName} Tool",
+                    Description = $"Select {toolInstance.DisplayName} Tool",
+                    IconPath = $"@{toolInstance.ImagePath}",
+                    IconEvaluator = IconEvaluator.Default,
+                    TransientKey = toolAttr.Transient,
+                    DefaultShortcut = toolAttr.GetShortcut(),
+                    Shortcut = GetShortcut(internalName, toolAttr.GetShortcut()),
+                    ToolType = type,
+                };
+
+                Commands.Add(command);
+                AddCommandToCommandsCollection(command);
             }
 
-            foreach (var commands in commandGroups)
+            // save all commands into CommandGroups
+            foreach (var (groupInternalName, storedCommands) in commands)
             {
-                CommandGroups.Add(new(commands.Key, commands.Value));
+                var groupData = commandGroupsData.Where(group => group.internalName == groupInternalName).FirstOrDefault();
+                string groupDisplayName;
+                if (groupData == default)
+                    groupDisplayName = "Misc";
+                else
+                    groupDisplayName = groupData.displayName;
+                CommandGroups.Add(new(groupDisplayName, storedCommands));
             }
 
-            KeyCombination GetShortcut(string name, KeyCombination defaultShortcut) => shortcuts.FirstOrDefault(x => x.Value.Contains(name), new(defaultShortcut, null)).Key;
+            KeyCombination GetShortcut(string internalName, KeyCombination defaultShortcut) 
+                => shortcuts.FirstOrDefault(x => x.Value.Contains(internalName), new(defaultShortcut, null)).Key;
 
-            void AddCommandToGroup(Command command)
+            void AddCommandToCommandsCollection(Command command)
             {
-                var display = groups.FirstOrDefault(x => x.Value.Any(x => command.Name.StartsWith(x))).Key;
-                if (display == null)
-                {
-                    display = "Misc";
-                }
-                commandGroups.Add(display, command);
+                (string internalName, string displayName) group = commandGroupsData.FirstOrDefault(x => command.Name.StartsWith(x.internalName));
+                if (group == default)
+                    commands.Add("", command);
+                else
+                    commands.Add(group.internalName, command);
             }
 
             void AddEvaluator<TAttr, T, TParameter>(MethodInfo method, object instance, TAttr attribute, IDictionary<string, T> evaluators)
@@ -180,7 +200,7 @@ namespace PixiEditor.Models.Commands
                 where TAttr : Evaluator.EvaluatorAttribute
                 => AddEvaluatorFactory<TAttr, T, TParameter>(method, instance, attribute, evaluators, x => new T() { Name = attribute.Name, Evaluate = x });
 
-            void AddEvaluatorFactory<TAttr, T, TParameter>(MethodInfo method, object instance, TAttr attribute, IDictionary<string, T> evaluators, Func<Func<object, TParameter>, T> factory)
+            void AddEvaluatorFactory<TAttr, T, TParameter>(MethodInfo method, object serviceInstance, TAttr attribute, IDictionary<string, T> evaluators, Func<Func<object, TParameter>, T> factory)
                 where T : Evaluator<TParameter>, new()
                 where TAttr : Evaluator.EvaluatorAttribute
             {
@@ -192,7 +212,7 @@ namespace PixiEditor.Models.Commands
                 {
                     throw new Exception($"Too many parameters for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name}");
                 }
-                else if (!method.IsStatic && instance is null)
+                else if (!method.IsStatic && serviceInstance is null)
                 {
                     throw new Exception($"No type instance for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name} found");
                 }
@@ -203,11 +223,11 @@ namespace PixiEditor.Models.Commands
 
                 if (parameters.Length == 1)
                 {
-                    func = x => (TParameter)method.Invoke(instance, new[] { CastParameter(x, parameters[0].ParameterType) });
+                    func = x => (TParameter)method.Invoke(serviceInstance, new[] { CastParameter(x, parameters[0].ParameterType) });
                 }
                 else
                 {
-                    func = x => (TParameter)method.Invoke(instance, null);
+                    func = x => (TParameter)method.Invoke(serviceInstance, null);
                 }
 
                 T evaluator = factory(func);
@@ -218,13 +238,8 @@ namespace PixiEditor.Models.Commands
             object CastParameter(object input, Type target)
             {
                 if (target == typeof(object) || target == input.GetType())
-                {
                     return input;
-                }
-                else
-                {
-                    return Convert.ChangeType(input, target);
-                }
+                return Convert.ChangeType(input, target);
             }
 
             TCommand AddCommand<TAttr, TCommand>(MethodInfo method, object instance, TAttr attribute, Func<bool, string, Action<object>, CanExecuteEvaluator, IconEvaluator, TCommand> commandFactory)
@@ -235,11 +250,11 @@ namespace PixiEditor.Models.Commands
                 {
                     if (method.GetParameters().Length > 1)
                     {
-                        throw new Exception($"Too many parameters for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name}");
+                        throw new Exception($"Too many parameters for the CanExecute evaluator '{attribute.InternalName}' at {method.ReflectedType.FullName}.{method.Name}");
                     }
                     else if (!method.IsStatic && instance is null)
                     {
-                        throw new Exception($"No type instance for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name} found");
+                        throw new Exception($"No type instance for the CanExecute evaluator '{attribute.InternalName}' at {method.ReflectedType.FullName}.{method.Name} found");
                     }
                 }
 
@@ -256,10 +271,10 @@ namespace PixiEditor.Models.Commands
                     action = x => method.Invoke(instance, new[] { x });
                 }
 
-                string name = attribute.Name;
-                bool isDebug = attribute.Name.StartsWith("#DEBUG#");
+                string name = attribute.InternalName;
+                bool isDebug = attribute.InternalName.StartsWith("#DEBUG#");
 
-                if (attribute.Name.StartsWith("#DEBUG#"))
+                if (attribute.InternalName.StartsWith("#DEBUG#"))
                 {
                     name = name["#DEBUG#".Length..];
                 }
@@ -272,7 +287,7 @@ namespace PixiEditor.Models.Commands
                         attribute.IconEvaluator != null ? IconEvaluators[attribute.IconEvaluator] : IconEvaluator.Default);
 
                 Commands.Add(command);
-                AddCommandToGroup(command);
+                AddCommandToCommandsCollection(command);
 
                 return command;
             }
@@ -290,7 +305,7 @@ namespace PixiEditor.Models.Commands
         }
 
         /// <summary>
-        /// Delets all shortcuts of <paramref name="newShortcut"/> and adds <paramref name="command"/>
+        /// Deletes all shortcuts of <paramref name="newShortcut"/> and adds <paramref name="command"/>
         /// </summary>
         public void ReplaceShortcut(Command command, KeyCombination newShortcut)
         {

+ 4 - 4
PixiEditor/Models/Commands/CommandGroup.cs

@@ -9,7 +9,7 @@ namespace PixiEditor.Models.Commands
         private readonly Command[] commands;
         private readonly Command[] visibleCommands;
 
-        public string Display { get; set; }
+        public string DisplayName { get; set; }
 
         public bool HasAssignedShortcuts { get; set; }
 
@@ -17,9 +17,9 @@ namespace PixiEditor.Models.Commands
 
         public IEnumerable<Command> VisibleCommands => visibleCommands;
 
-        public CommandGroup(string display, IEnumerable<Command> commands)
+        public CommandGroup(string displayName, IEnumerable<Command> commands)
         {
-            Display = display;
+            DisplayName = displayName;
             this.commands = commands.ToArray();
             visibleCommands = commands.Where(x => !string.IsNullOrEmpty(x.Display)).ToArray();
 
@@ -30,7 +30,7 @@ namespace PixiEditor.Models.Commands
             }
         }
 
-        private void Command_ShortcutChanged(Command _, ShortcutChangedEventArgs args)
+        private void Command_ShortcutChanged(Command cmd, ShortcutChangedEventArgs args)
         {
             if ((args.NewShortcut != KeyCombination.None && HasAssignedShortcuts) ||
                 (args.NewShortcut == KeyCombination.None && !HasAssignedShortcuts))

+ 1 - 1
PixiEditor/Models/Commands/CommandMethods.cs

@@ -23,5 +23,5 @@ public class CommandMethods
         }
     }
 
-    public bool CanExecute(object parameter) => _canExecute.EvaluateEvaluator(_command, parameter);
+    public bool CanExecute(object parameter) => _canExecute.CallEvaluate(_command, parameter);
 }

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

@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Commands
         }
 
         public event ShortcutChangedEventHandler ShortcutChanged;
-        
+
         protected abstract object GetParameter();
 
         protected Command(Action<object> onExecute, CanExecuteEvaluator canExecute) =>
@@ -50,8 +50,8 @@ namespace PixiEditor.Models.Commands
 
         public bool CanExecute() => Methods.CanExecute(GetParameter());
 
-        public ImageSource GetIcon() => IconEvaluator.EvaluateEvaluator(this, GetParameter());
-        
+        public ImageSource GetIcon() => IconEvaluator.CallEvaluate(this, GetParameter());
+
         public delegate void ShortcutChangedEventHandler(Command command, ShortcutChangedEventArgs args);
     }
 }

+ 1 - 1
PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs

@@ -15,7 +15,7 @@
                 this.value = value;
             }
 
-            public override bool EvaluateEvaluator(Command command, object parameter) => value;
+            public override bool CallEvaluate(Command command, object parameter) => value;
         }
     }
 }

+ 5 - 2
PixiEditor/Models/Commands/Evaluators/Evaluator.cs

@@ -9,6 +9,9 @@ namespace PixiEditor.Models.Commands.Evaluators
 
         public Func<object, T> Evaluate { private get; init; }
 
-        public virtual T EvaluateEvaluator(Command command, object parameter) => Evaluate(parameter);
+        /// <param name="command">The command this evaluator corresponds to</param>
+        /// <param name="parameter">The parameter to pass to the Evaluate function</param>
+        /// <returns>The value returned by the Evaluate function</returns>
+        public virtual T CallEvaluate(Command command, object parameter) => Evaluate(parameter);
     }
-}
+}

+ 3 - 3
PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs

@@ -10,8 +10,8 @@ namespace PixiEditor.Models.Commands.Evaluators
     {
         public static IconEvaluator Default { get; } = new CommandNameEvaluator();
 
-        public override ImageSource EvaluateEvaluator(Command command, object parameter) =>
-            base.EvaluateEvaluator(command, parameter ?? command);
+        public override ImageSource CallEvaluate(Command command, object parameter) =>
+            base.CallEvaluate(command, parameter ?? command);
 
         [DebuggerDisplay("IconEvaluator.Default")]
         private class CommandNameEvaluator : IconEvaluator
@@ -20,7 +20,7 @@ namespace PixiEditor.Models.Commands.Evaluators
 
             public static Dictionary<string, BitmapImage> images = new();
 
-            public override ImageSource EvaluateEvaluator(Command command, object parameter)
+            public override ImageSource CallEvaluate(Command command, object parameter)
             {
                 string path;
 

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

@@ -11,7 +11,7 @@ namespace PixiEditor.Models.Commands.Search
 
         public override bool CanExecute => Command.CanExecute();
 
-        public override ImageSource Icon => Command.IconEvaluator.EvaluateEvaluator(Command, this);
+        public override ImageSource Icon => Command.IconEvaluator.CallEvaluate(Command, this);
 
         public override KeyCombination Shortcut => Command.Shortcut;
 

+ 2 - 2
PixiEditor/Models/Commands/ShortcutFile.cs

@@ -24,7 +24,7 @@ namespace PixiEditor.Models.Commands
 
         public void SaveShortcuts()
         {
-            EnumerableDictionary<KeyCombination, string> shortcuts = new();
+            OneToManyDictionary<KeyCombination, string> shortcuts = new();
 
             foreach (var shortcut in _commands.Commands.GetShortcuts())
             {
@@ -37,7 +37,7 @@ namespace PixiEditor.Models.Commands
             File.WriteAllText(Path, JsonConvert.SerializeObject(shortcuts));
         }
 
-        public IEnumerable<KeyValuePair<KeyCombination, IEnumerable<string>>> GetShortcuts() =>
+        public IEnumerable<KeyValuePair<KeyCombination, IEnumerable<string>>> LoadShortcuts() =>
             JsonConvert.DeserializeObject<IEnumerable<KeyValuePair<KeyCombination, IEnumerable<string>>>>(File.ReadAllText(Path));
     }
 }

+ 1 - 1
PixiEditor/Models/Commands/XAML/Command.cs

@@ -36,7 +36,7 @@ namespace PixiEditor.Models.Commands.XAML
                     new Commands.Command.BasicCommand(null, null)
                     {
                         Name = Name,
-                        Display = attribute.Display,
+                        Display = attribute.DisplayName,
                         Description = attribute.Description,
                         DefaultShortcut = attribute.GetShortcut(),
                         Shortcut = attribute.GetShortcut()

+ 0 - 150
PixiEditor/Models/DataHolders/EnumerableDictionary.cs

@@ -1,150 +0,0 @@
-using System.Collections;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-
-namespace PixiEditor.Models.DataHolders
-{
-    [DebuggerDisplay("Count = {Count}")]
-    public class EnumerableDictionary<TKey, T> : ICollection<KeyValuePair<TKey, IEnumerable<T>>>
-    {
-        private readonly Dictionary<TKey, List<T>> _dictionary;
-
-        public EnumerableDictionary()
-        {
-            _dictionary = new Dictionary<TKey, List<T>>();
-        }
-
-        public int Count => _dictionary.Count;
-
-        public bool IsReadOnly => false;
-
-        [NotNull]
-        public IEnumerable<T> this[TKey key]
-        {
-            get
-            {
-                if (_dictionary.TryGetValue(key, out List<T> values))
-                {
-                    return values;
-                }
-
-                List<T> newList = new();
-                _dictionary.Add(key, newList);
-
-                return newList;
-            }
-        }
-
-        public void Add(TKey key, T value)
-        {
-            if (_dictionary.TryGetValue(key, out List<T> values))
-            {
-                values.Add(value);
-                return;
-            }
-
-            _dictionary.Add(key, new() { value });
-        }
-
-        public void AddRange(TKey key, IEnumerable<T> enumerable)
-        {
-            if (_dictionary.TryGetValue(key, out List<T> values))
-            {
-                foreach (T value in enumerable)
-                {
-                    values.Add(value);
-                }
-
-                return;
-            }
-
-            _dictionary.Add(key, new(enumerable));
-        }
-
-        public void Add(KeyValuePair<TKey, IEnumerable<T>> item) => AddRange(item.Key, item.Value);
-
-        public void Clear() => _dictionary.Clear();
-
-        public void Clear(TKey key)
-        {
-            if (_dictionary.TryGetValue(key, out List<T> value))
-            {
-                value.Clear();
-            }
-        }
-
-        public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
-
-        public bool Contains(KeyValuePair<TKey, IEnumerable<T>> item)
-        {
-            if (_dictionary.TryGetValue(item.Key, out List<T> values))
-            {
-                return item.Value.All(x => values.Contains(x));
-            }
-
-            return false;
-        }
-
-        public void CopyTo(KeyValuePair<TKey, IEnumerable<T>>[] array, int arrayIndex)
-        {
-            var enumerator = GetEnumerator();
-
-            for (int i = arrayIndex; i < array.Length; i++)
-            {
-                if (!enumerator.MoveNext())
-                {
-                    break;
-                }
-
-                array[i] = enumerator.Current;
-            }
-        }
-
-        public IEnumerator<KeyValuePair<TKey, IEnumerable<T>>> GetEnumerator()
-        {
-            foreach (var pair in _dictionary)
-            {
-                yield return new(pair.Key, pair.Value);
-            }
-        }
-
-        public bool Remove(KeyValuePair<TKey, IEnumerable<T>> item)
-        {
-            if (!_dictionary.TryGetValue(item.Key, out List<T> values))
-            {
-                return false;
-            }
-
-            bool success = true;
-
-            foreach (var enumerableItem in item.Value)
-            {
-                success &= values.Remove(enumerableItem);
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Removes <paramref name="item"/> from all enumerables in the dictionary.
-        /// Returns true if any entry was removed
-        /// </summary>
-        public bool Remove(T item)
-        {
-            bool anyRemoved = false;
-
-            foreach (var enumItem in _dictionary)
-            {
-                anyRemoved |= enumItem.Value.Remove(item);
-            }
-
-            return anyRemoved;
-        }
-
-        public bool Remove(TKey key, T item) => _dictionary[key].Remove(item);
-
-        public bool Remove(TKey key) => _dictionary.Remove(key);
-
-        IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
-    }
-}

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

@@ -20,12 +20,12 @@ namespace PixiEditor.Models.Services
         public CanExecuteEvaluator GetCanExecute(string name) => _controller.CanExecuteEvaluators[name];
 
         public bool CanExecute(string name, Command command, object argument) =>
-            _controller.CanExecuteEvaluators[name].EvaluateEvaluator(command, argument);
+            _controller.CanExecuteEvaluators[name].CallEvaluate(command, argument);
 
         public IconEvaluator GetIconEvaluator(string name) => _controller.IconEvaluators[name];
 
         public ImageSource GetIcon(string name, Command command, object argument) =>
-            _controller.IconEvaluators[name].EvaluateEvaluator(command, argument);
+            _controller.IconEvaluators[name].CallEvaluate(command, argument);
 
         public ICommand GetICommand(string name, bool useProvidedArgument = false) => XAMLCommand.GetICommand(_controller.Commands[name], useProvidedArgument);
     }

+ 1 - 2
PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -1,6 +1,5 @@
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands;
-using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.ViewModels.SubViewModels.UserPreferences;
 using System.Windows;
@@ -115,7 +114,7 @@ namespace PixiEditor.ViewModels
 
             public GroupSearchResult(CommandGroup group)
             {
-                Display = group.Display;
+                Display = group.DisplayName;
                 Commands = new(group.VisibleCommands.Select(x => new CommandSearchResult(x)));
             }
         }

+ 1 - 1
PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs

@@ -63,7 +63,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner.BitmapManager.CloseDocument(document);
         }
 
-        [Command.Basic("PixiEditor.Document.DeletePixels", "Delete pixels", "Delete selected pixels", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete, Icon = "Tools/EraserImage.png")]
+        [Command.Basic("PixiEditor.Document.DeletePixels", "Delete pixels", "Delete selected pixels", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete, IconPath = "Tools/EraserImage.png")]
         public void DeletePixels()
         {
             var doc = Owner.BitmapManager.ActiveDocument;

+ 1 - 1
PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -203,7 +203,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             return Owner.BitmapManager.ActiveDocument?.Layers.Count(x => x.IsActive) > 0;
         }
 
-        [Command.Basic("PixiEditor.Layer.New", "New Layer", "Create new layer", CanExecute = "PixiEditor.HasDocument", Key = Key.N, Modifiers = ModifierKeys.Control | ModifierKeys.Shift, Icon = "Layer-add.png")]
+        [Command.Basic("PixiEditor.Layer.New", "New Layer", "Create new layer", CanExecute = "PixiEditor.HasDocument", Key = Key.N, Modifiers = ModifierKeys.Control | ModifierKeys.Shift, IconPath = "Layer-add.png")]
         public void NewLayer(object parameter)
         {
             GuidStructureItem control = GetGroupFromParameter(parameter);

+ 2 - 2
PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs

@@ -22,7 +22,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         ///     Redo last action.
         /// </summary>
         [Command.Basic("PixiEditor.Undo.Redo", "Redo", "Redo next step", CanExecute = "PixiEditor.Undo.CanRedo", Key = Key.Y, Modifiers = ModifierKeys.Control,
-                       Icon = "E7A6", IconEvaluator = "PixiEditor.FontIcon")]
+                       IconPath = "E7A6", IconEvaluator = "PixiEditor.FontIcon")]
         public void Redo()
         {
             UndoRedoCalled?.Invoke(this, EventArgs.Empty);
@@ -39,7 +39,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         ///     Undo last action.
         /// </summary>
         [Command.Basic("PixiEditor.Undo.Undo", "Undo", "Undo previous step", CanExecute = "PixiEditor.Undo.CanUndo", Key = Key.Z, Modifiers = ModifierKeys.Control,
-         Icon = "E7A7", IconEvaluator = "PixiEditor.FontIcon")]
+         IconPath = "E7A7", IconEvaluator = "PixiEditor.FontIcon")]
         public void Undo()
         {
             UndoRedoCalled?.Invoke(this, EventArgs.Empty);

+ 1 - 0
PixiEditor/Views/Dialogs/OptionsPopup.xaml

@@ -6,6 +6,7 @@
         xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
         xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
         mc:Ignorable="d"
+        WindowStartupLocation="CenterScreen"
         SizeToContent="WidthAndHeight"
         x:Name="popup"
         Background="{StaticResource AccentColor}" Foreground="White">

+ 0 - 3
PixiEditor/Views/Dialogs/OptionsPopup.xaml.cs

@@ -4,9 +4,6 @@ using System.Windows;
 
 namespace PixiEditor.Views.Dialogs
 {
-    /// <summary>
-    /// Interaction logic for AdvancedDialogPopup.xaml
-    /// </summary>
     public partial class OptionPopup : Window
     {
         public static readonly DependencyProperty PopupContentProperty =

+ 18 - 19
PixiEditor/Views/UserControls/ShortcutBox.cs

@@ -3,6 +3,7 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Input;
 
 namespace PixiEditor.Views.UserControls
 {
@@ -48,27 +49,25 @@ namespace PixiEditor.Views.UserControls
             {
                 if (controller.Commands[e].Any())
                 {
-                    OptionsDialog<string> dialog = new("Already assigned", $"This shortcut is already asigned to '{controller.Commands[e].First().Display}'\nDo you want to replace the shortcut or switch shortcuts?")
+                    var oldShortcut = Command.Shortcut;
+                    bool enableSwap = oldShortcut is not { Key: Key.None, Modifiers: ModifierKeys.None };
+                    
+                    string text = enableSwap ?
+                        $"This shortcut is already assigned to '{controller.Commands[e].First().Display}'\nDo you want to replace the existing shortcut or swap the two?" :
+                        $"This shortcut is already assigned to '{controller.Commands[e].First().Display}'\nDo you want to replace the existing shortcut?";
+                    OptionsDialog<string> dialog = new("Already assigned", text);
+                    
+                    dialog.Add("Replace", x => controller.ReplaceShortcut(Command, e));
+                    if (enableSwap)
                     {
+                        dialog.Add("Swap", x =>
                         {
-                            "Replace", x => controller.ReplaceShortcut(Command, e) 
-                        },
-                        {
-                            "Switch", x =>
-                            {
-                                var oldCommand = controller.Commands[e].First();
-                                var oldShortcut = Command.Shortcut;
-                                controller.ReplaceShortcut(Command, e);
-                                controller.ReplaceShortcut(oldCommand, oldShortcut);
-                            }
-                        },
-                        {
-                            "Abort", x =>
-                            {
-                                box.KeyCombination = Command.Shortcut;
-                            }
-                        }
-                    };
+                            var oldCommand = controller.Commands[e].First();
+                            controller.ReplaceShortcut(Command, e);
+                            controller.ReplaceShortcut(oldCommand, oldShortcut);
+                        });
+                    }
+                    dialog.Add("Cancel", x => box.KeyCombination = Command.Shortcut);
 
                     dialog.ShowDialog();
                     changingCombination = false;