Browse Source

Command Manager WIP

CPKreuz 3 năm trước cách đây
mục cha
commit
4b307d445a
28 tập tin đã thay đổi với 809 bổ sung44 xóa
  1. 1 5
      PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  2. 2 0
      PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  3. 37 24
      PixiEditor/Helpers/InputKeyHelpers.cs
  4. 36 0
      PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs
  5. 32 0
      PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  6. 12 0
      PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs
  7. 15 0
      PixiEditor/Models/Commands/Attributes/Evaluators/EvaluatorAttribute.cs
  8. 14 0
      PixiEditor/Models/Commands/Attributes/Evaluators/FactoryAttribute.cs
  9. 57 0
      PixiEditor/Models/Commands/CommandCollection.cs
  10. 149 0
      PixiEditor/Models/Commands/CommandController.cs
  11. 23 0
      PixiEditor/Models/Commands/CommandMethods.cs
  12. 14 0
      PixiEditor/Models/Commands/Commands/BasicCommand.cs
  13. 68 0
      PixiEditor/Models/Commands/Commands/Command.cs
  14. 6 0
      PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs
  15. 12 0
      PixiEditor/Models/Commands/Evaluators/Evaluator.cs
  16. 6 0
      PixiEditor/Models/Commands/Evaluators/FactoryEvaluator.cs
  17. 24 0
      PixiEditor/Models/Commands/ShortcutFile.cs
  18. 28 0
      PixiEditor/Models/Commands/XAML/Command.cs
  19. 41 0
      PixiEditor/Models/Commands/XAML/ShortcutBinding.cs
  20. 4 0
      PixiEditor/Models/Controllers/BitmapManager.cs
  21. 146 0
      PixiEditor/Models/DataHolders/EnumerableDictionary.cs
  22. 44 0
      PixiEditor/Models/DataHolders/KeyCombination.cs
  23. 1 0
      PixiEditor/PixiEditor.csproj
  24. 6 10
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  25. 23 0
      PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs
  26. 5 0
      PixiEditor/ViewModels/ViewModelMain.cs
  27. 2 2
      PixiEditor/Views/MainWindow.xaml
  28. 1 3
      PixiEditor/Views/UserControls/NumberInput.xaml.cs

+ 1 - 5
PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -11,11 +11,7 @@ namespace PixiEditor.Helpers.Converters
         {
             if (value is Key key)
             {
-                return key switch
-                {
-                    Key.Space => "Space",
-                    _ => InputKeyHelpers.GetCharFromKey(key),
-                };
+                return InputKeyHelpers.GetKeyboardKey(key);
             }
             else if (value is ModifierKeys)
             {

+ 2 - 0
PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -1,4 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers.Shortcuts;
 using PixiEditor.Models.Services;
@@ -42,6 +43,7 @@ namespace PixiEditor.Helpers.Extensions
                 .AddSingleton<DebugViewModel>()
                 // Controllers
                 .AddSingleton<ShortcutController>()
+                .AddSingleton<CommandController>()
                 .AddSingleton<BitmapManager>()
                 // Tools
                 .AddSingleton<Tool, MoveViewportTool>()

+ 37 - 24
PixiEditor/Helpers/InputKeyHelpers.cs

@@ -1,43 +1,56 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Globalization;
 using System.Runtime.InteropServices;
 using System.Text;
-using System.Threading.Tasks;
 using System.Windows.Input;
 
 namespace PixiEditor.Helpers
 {
     public static class InputKeyHelpers
     {
-        public static string GetCharFromKey(Key key)
+        /// <summary>
+        /// Returns the charcter of the <paramref name="key"/> mapped to the users keyboard layout
+        /// </summary>
+        public static string GetKeyboardKey(Key key) => GetKeyboardKey(key, CultureInfo.CurrentCulture);
+
+        public static string GetKeyboardKey(Key key, CultureInfo culture) => key switch
+        {
+            Key.NumPad0 => "Num0",
+            Key.NumPad1 => "Num1",
+            Key.NumPad2 => "Num2",
+            Key.NumPad3 => "Num3",
+            Key.NumPad4 => "Num4",
+            Key.NumPad5 => "Num5",
+            Key.NumPad6 => "Num6",
+            Key.NumPad7 => "Num7",
+            Key.NumPad8 => "Num8",
+            Key.NumPad9 => "Num9",
+            Key.Space => nameof(Key.Space),
+            Key.Tab => nameof(Key.Tab),
+            Key.Back => nameof(Key.Back),
+            Key.Escape => "Esc",
+            _ => GetMappedKey(key, culture),
+        };
+
+        private static string GetMappedKey(Key key, CultureInfo culture)
         {
             int virtualKey = KeyInterop.VirtualKeyFromKey(key);
             byte[] keyboardState = new byte[256];
-            GetKeyboardState(keyboardState);
 
-            uint scanCode = MapVirtualKeyW((uint)virtualKey, MapType.MAPVK_VK_TO_VSC);
-            StringBuilder stringBuilder = new (3);
+            uint scanCode = MapVirtualKeyExW((uint)virtualKey, MapType.MAPVK_VK_TO_VSC, culture.KeyboardLayoutId);
+            StringBuilder stringBuilder = new(3);
 
             int result = ToUnicode((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0);
 
-            switch (result)
-            {
-                case 0:
-                    {
-                        return key.ToString();
-                    }
+            string stringResult;
 
-                case -1:
-                    {
-                        return stringBuilder.ToString().ToUpper();
-                    }
+            stringResult = result switch
+            {
+                0 => key.ToString(),
+                -1 => stringBuilder.ToString().ToUpper(),
+                _ => stringBuilder[result - 1].ToString().ToUpper()
+            };
 
-                default:
-                    {
-                        return stringBuilder[result - 1].ToString().ToUpper();
-                    }
-            }
+            return stringResult;
         }
 
         private enum MapType : uint
@@ -77,6 +90,6 @@ namespace PixiEditor.Helpers
         private static extern bool GetKeyboardState(byte[] lpKeyState);
 
         [DllImport("user32.dll")]
-        private static extern uint MapVirtualKeyW(uint uCode, MapType uMapType);
+        private static extern uint MapVirtualKeyExW(uint uCode, MapType uMapType, int hkl);
     }
 }

+ 36 - 0
PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs

@@ -0,0 +1,36 @@
+namespace PixiEditor.Models.Commands.Attributes;
+
+public partial class Command
+{
+    public class BasicAttribute : CommandAttribute
+    {
+        public object Parameter { get; set; }
+
+        /// <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="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>
+        /// <param name="key">The default shortcut key of the command</param>
+        /// <param name="modifiers">The default shortcut modifier keys of the command</param>
+        public BasicAttribute(string name, string display, string description)
+            : this(name, null, display, 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="display">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>
+        /// <param name="key">The default shortcut key of the command</param>
+        /// <param name="modifiers">The default shortcut modifier keys of the command</param>
+        public BasicAttribute(string name, object parameter, string display, string description)
+            : base(name, display, description)
+        {
+            Parameter = parameter;
+        }
+    }
+}

+ 32 - 0
PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -0,0 +1,32 @@
+using PixiEditor.Models.DataHolders;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.Commands.Attributes;
+
+public partial class Command
+{
+    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
+    public abstract class CommandAttribute : Attribute
+    {
+        public string Name { get; }
+
+        public string Display { get; }
+
+        public string Description { get; }
+
+        public string CanExecute { get; set; }
+
+        public Key Key { get; set; }
+
+        public ModifierKeys Modifiers { get; set; }
+
+        protected CommandAttribute(string name, string display, string description)
+        {
+            Name = name;
+            Display = display;
+            Description = description;
+        }
+
+        public KeyCombination GetShortcut() => new() { Key = Key, Modifiers = Modifiers };
+    }
+}

+ 12 - 0
PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.Models.Commands.Attributes;
+
+public partial class Evaluator
+{
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
+    public class CanExecuteAttribute : EvaluatorAttribute
+    {
+        public CanExecuteAttribute(string name)
+            : base(name)
+        { }
+    }
+}

+ 15 - 0
PixiEditor/Models/Commands/Attributes/Evaluators/EvaluatorAttribute.cs

@@ -0,0 +1,15 @@
+namespace PixiEditor.Models.Commands.Attributes;
+
+public static partial class Evaluator
+{
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
+    public abstract class EvaluatorAttribute : Attribute
+    {
+        public string Name { get; }
+
+        public EvaluatorAttribute(string name)
+        {
+            Name = name;
+        }
+    }
+}

+ 14 - 0
PixiEditor/Models/Commands/Attributes/Evaluators/FactoryAttribute.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Models.Commands.Attributes;
+
+public partial class Evaluator
+{
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
+    public class FactoryAttribute : EvaluatorAttribute
+    {
+        public object Parameter { get; set; }
+
+        public FactoryAttribute(string name)
+            : base(name)
+        { }
+    }
+}

+ 57 - 0
PixiEditor/Models/Commands/CommandCollection.cs

@@ -0,0 +1,57 @@
+using PixiEditor.Models.DataHolders;
+using System.Collections;
+using System.Diagnostics;
+
+namespace PixiEditor.Models.Commands
+{
+    [DebuggerDisplay("Count = {Count}")]
+    public class CommandCollection : ICollection<Command>
+    {
+        private readonly Dictionary<string, Command> _commandNames;
+        private readonly EnumerableDictionary<KeyCombination, Command> _commandShortcuts;
+
+        public int Count => _commandNames.Count;
+
+        public bool IsReadOnly => false;
+
+        public Command this[string name] => _commandNames[name];
+
+        public IEnumerable<Command> this[KeyCombination shortcut] => _commandShortcuts[shortcut];
+
+        public CommandCollection()
+        {
+            _commandNames = new();
+            _commandShortcuts = new();
+        }
+
+        public void Add(Command item)
+        {
+            _commandNames.Add(item.Name, item);
+            _commandShortcuts.Add(item.Shortcut, item);
+        }
+
+        public void Clear()
+        {
+            _commandNames.Clear();
+            _commandShortcuts.Clear();
+        }
+
+        public bool Contains(Command item) => _commandNames.ContainsKey(item.Name);
+
+        public void CopyTo(Command[] array, int arrayIndex) => throw new NotImplementedException();
+
+        public IEnumerator<Command> GetEnumerator() => _commandNames.Values.GetEnumerator();
+
+        public bool Remove(Command item)
+        {
+            bool anyRemoved = false;
+
+            anyRemoved |= _commandNames.Remove(item.Name);
+            anyRemoved |= _commandShortcuts.Remove(item);
+
+            return anyRemoved;
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}

+ 149 - 0
PixiEditor/Models/Commands/CommandController.cs

@@ -0,0 +1,149 @@
+using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Evaluators;
+using System.Reflection;
+using CommandAttribute = PixiEditor.Models.Commands.Attributes.Command;
+
+namespace PixiEditor.Models.Commands
+{
+    public class CommandController
+    {
+        public CommandCollection Commands { get; set; }
+
+        public Dictionary<string, FactoryEvaluator> FactoryEvaluators { get; set; }
+
+        public Dictionary<string, CanExecuteEvaluator> CanExecuteEvaluators { get; set; }
+
+        public CommandController(IServiceProvider services)
+        {
+            Commands = new();
+            FactoryEvaluators = new();
+            CanExecuteEvaluators = new();
+
+            Init(services);
+        }
+
+        public void Init(IServiceProvider services)
+        {
+            var types = typeof(CommandController).Assembly.GetTypes();
+
+            foreach (var type in types)
+            {
+                object instanceType = null;
+                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)
+                        {
+                            AddEvaluator<Evaluator.CanExecuteAttribute, CanExecuteEvaluator, bool>(method, instanceType, canExecute, CanExecuteEvaluators);
+                        }
+                        else if (attribute is Evaluator.FactoryAttribute factory)
+                        {
+                            AddEvaluator<Evaluator.FactoryAttribute, FactoryEvaluator, object>(method, instanceType, factory, FactoryEvaluators);
+                        }
+                    }
+                }
+
+                foreach (var method in methods)
+                {
+                    var commandAttrs = method.GetCustomAttributes<CommandAttribute.BasicAttribute>();
+
+                    if (instanceType is null && commandAttrs.Any())
+                    {
+                        instanceType = services.GetService(type);
+                    }
+
+                    foreach (var attribute in commandAttrs)
+                    {
+                        AddCommand(method, instanceType, attribute, (x, xCan) => new Command.BasicCommand
+                        {
+                            Name = attribute.Name,
+                            Display = attribute.Display,
+                            Description = attribute.Description,
+                            Methods = new(x, xCan),
+                            DefaultShortcut = attribute.GetShortcut(),
+                            Parameter = attribute.Parameter,
+                            Shortcut = attribute.GetShortcut(),
+                        });
+                    }
+                }
+            }
+
+            void AddEvaluator<TAttr, T, TParameter>(MethodInfo method, object instance, TAttr attribute, IDictionary<string, T> evaluators)
+                where T : Evaluator<TParameter>, new()
+                where TAttr : Evaluator.EvaluatorAttribute
+            {
+                if (method.ReturnType != typeof(TParameter))
+                {
+                    throw new Exception($"Invalid return type for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name}\nExpected '{typeof(TParameter).FullName}'");
+                }
+                else if (method.GetParameters().Length > 1)
+                {
+                    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)
+                {
+                    throw new Exception($"No type instance for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name} found");
+                }
+
+                var parameters = method.GetParameters();
+
+                Func<object, TParameter> func;
+
+                if (parameters.Length == 1)
+                {
+                    func = x => (TParameter)method.Invoke(instance, new[] { Convert.ChangeType(x, parameters[0].ParameterType) });
+                }
+                else
+                {
+                    func = x => (TParameter)method.Invoke(instance, null);
+                }
+
+                T evaluator = new()
+                {
+                    Name = attribute.Name,
+                    Evaluate = func
+                };
+
+                evaluators.Add(evaluator.Name, evaluator);
+            }
+
+            void AddCommand<TAttr, TCommand>(MethodInfo method, object instance, TAttr attribute, Func<Action<object>, Predicate<object>, TCommand> commandFactory)
+                where TAttr : CommandAttribute.CommandAttribute
+                where TCommand : Command
+            {if (method.GetParameters().Length > 1)
+                {
+                    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)
+                {
+                    throw new Exception($"No type instance for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name} found");
+                }
+
+                var parameters = method.GetParameters();
+
+                Action<object> action;
+
+                if (parameters.Length == 1)
+                {
+                    action = x => method.Invoke(instance, new[] { x });
+                }
+                else
+                {
+                    action = x => method.Invoke(instance, null);
+                }
+
+                Commands.Add(commandFactory(action, x => CanExecuteEvaluators[attribute.CanExecute].Evaluate(x)));
+            }
+        }
+    }
+}

+ 23 - 0
PixiEditor/Models/Commands/CommandMethods.cs

@@ -0,0 +1,23 @@
+namespace PixiEditor.Models.Commands;
+
+public class CommandMethods
+{
+    private readonly Action<object> _execute;
+    private readonly Predicate<object> _canExecute;
+
+    public CommandMethods(Action<object> execute, Predicate<object> canExecute)
+    {
+        _execute = execute;
+        _canExecute = canExecute;
+    }
+
+    public void Execute(object parameter)
+    {
+        if (CanExecute(parameter))
+        {
+            _execute(parameter);
+        }
+    }
+
+    public bool CanExecute(object parameter) => _canExecute(parameter);
+}

+ 14 - 0
PixiEditor/Models/Commands/Commands/BasicCommand.cs

@@ -0,0 +1,14 @@
+using PixiEditor.Models.DataHolders;
+
+namespace PixiEditor.Models.Commands
+{
+    public partial class Command
+    {
+        public class BasicCommand : Command
+        {
+            public object Parameter { get; init; }
+
+            protected override object GetParameter() => Parameter;
+        }
+    }
+}

+ 68 - 0
PixiEditor/Models/Commands/Commands/Command.cs

@@ -0,0 +1,68 @@
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using System.Diagnostics;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.Commands
+{
+    [DebuggerDisplay("{Name,nq} ('{Display,nq}')")]
+    public abstract partial class Command : NotifyableObject
+    {
+        private KeyCombination _shortcut;
+
+        public string Name { get; init; }
+
+        public string Display { get; init; }
+
+        public string Description { get; init; }
+
+        public CommandMethods Methods { get; init; }
+
+        public KeyCombination DefaultShortcut { get; init; }
+
+        public KeyCombination Shortcut
+        {
+            get => _shortcut;
+            set => SetProperty(ref _shortcut, value);
+        }
+
+        protected abstract object GetParameter();
+
+        public void Execute() => Methods.Execute(GetParameter());
+
+        public bool CanExecute() => Methods.CanExecute(GetParameter());
+
+        public ICommand GetICommand(bool useProvidedParameter) => new ProvidedICommand()
+        {
+            Command = this,
+            UseProvidedParameter = useProvidedParameter,
+        };
+
+        class ProvidedICommand : ICommand
+        {
+            public event EventHandler CanExecuteChanged
+            {
+                add => CommandManager.RequerySuggested += value;
+                remove => CommandManager.RequerySuggested -= value;
+            }
+
+            public Command Command { get; init; }
+
+            public bool UseProvidedParameter { 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();
+                }
+            }
+        }
+    }
+}

+ 6 - 0
PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Commands.Evaluators
+{
+    public class CanExecuteEvaluator : Evaluator<bool>
+    {
+    }
+}

+ 12 - 0
PixiEditor/Models/Commands/Evaluators/Evaluator.cs

@@ -0,0 +1,12 @@
+using System.Diagnostics;
+
+namespace PixiEditor.Models.Commands.Evaluators
+{
+    [DebuggerDisplay("{Name,nq}")]
+    public abstract class Evaluator<T>
+    {
+        public string Name { get; init; }
+
+        public Func<object, T> Evaluate { get; init; }
+    }
+}

+ 6 - 0
PixiEditor/Models/Commands/Evaluators/FactoryEvaluator.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Commands.Evaluators
+{
+    public class FactoryEvaluator : Evaluator<object>
+    {
+    }
+}

+ 24 - 0
PixiEditor/Models/Commands/ShortcutFile.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.Commands
+{
+    public class ShortcutFile
+    {
+        public string Path { get; }
+
+        public ShortcutFile(string path)
+        {
+            Path = path;
+        }
+
+        public void SaveShortcut(string name, Key key, ModifierKeys modifiers)
+        {
+
+        }
+    }
+}

+ 28 - 0
PixiEditor/Models/Commands/XAML/Command.cs

@@ -0,0 +1,28 @@
+using PixiEditor.ViewModels;
+using System.Windows.Markup;
+
+namespace PixiEditor.Models.Commands.XAML
+{
+    public class Command : MarkupExtension
+    {
+        private static CommandController commandController;
+
+        public string Name { get; set; }
+
+        public bool UseProvided { get; set; }
+
+        public Command() { }
+
+        public Command(string name) => Name = name;
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            if (commandController == null)
+            {
+                commandController = ViewModelMain.Current.CommandController;
+            }
+
+            return commandController.Commands[Name].GetICommand(UseProvided);
+        }
+    }
+}

+ 41 - 0
PixiEditor/Models/Commands/XAML/ShortcutBinding.cs

@@ -0,0 +1,41 @@
+using PixiEditor.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+using System.Windows.Markup;
+using ActualCommand = PixiEditor.Models.Commands.Command;
+
+namespace PixiEditor.Models.Commands.XAML
+{
+    public class ShortcutBinding : MarkupExtension
+    {
+        private static CommandController commandController;
+
+        public string Name { get; set; }
+
+        public ShortcutBinding() { }
+
+        public ShortcutBinding(string name) => Name = name;
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            if (commandController == null)
+            {
+                commandController = ViewModelMain.Current.CommandController;
+            }
+
+            return GetBinding(commandController.Commands[Name]).ProvideValue(serviceProvider);
+        }
+
+        public static Binding GetBinding(ActualCommand command) => new Binding
+        {
+            Source = command,
+            Path = new("Shortcut"),
+            Mode = BindingMode.OneWay,
+            StringFormat = ""
+        };
+    }
+}

+ 4 - 0
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -11,6 +11,7 @@ using System;
 using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.Windows;
+using PixiEditor.Models.Commands.Attributes;
 
 namespace PixiEditor.Models.Controllers
 {
@@ -110,6 +111,9 @@ namespace PixiEditor.Models.Controllers
             _highlightColor = new SKColor(0, 0, 0, 77);
         }
 
+        [Evaluator.CanExecute("PixiEditor.HasDocument")]
+        public bool DocumentNotNull() => ActiveDocument != null;
+
         public void CloseDocument(Document document)
         {
             int nextIndex = 0;

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

@@ -0,0 +1,146 @@
+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;
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
+    }
+}

+ 44 - 0
PixiEditor/Models/DataHolders/KeyCombination.cs

@@ -0,0 +1,44 @@
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.DataHolders
+{
+    [DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
+    public record struct KeyCombination(Key Key, ModifierKeys Modifiers)
+    {
+        public static KeyCombination None => new(Key.None, ModifierKeys.None);
+
+        public override string ToString() => ToString(CultureInfo.CurrentCulture);
+
+        public string ToString(CultureInfo culture)
+        {
+            StringBuilder builder = new();
+
+            foreach (ModifierKeys modifier in Modifiers.GetFlags())
+            {
+                if (modifier == ModifierKeys.None) continue;
+
+                string key = modifier switch
+                {
+                    ModifierKeys.Control => "Ctrl",
+                    _ => modifier.ToString()
+                };
+
+                builder.Append($"{key}+");
+            }
+
+            if (Key != Key.None)
+            {
+                builder.Append(InputKeyHelpers.GetKeyboardKey(Key, culture));
+            }
+
+            return builder.ToString();
+        }
+
+        private string GetDebuggerDisplay() => ToString(CultureInfo.InvariantCulture);
+    }
+}

+ 1 - 0
PixiEditor/PixiEditor.csproj

@@ -17,6 +17,7 @@
 		<Configurations>Debug;Release;MSIX;MSIX Debug;Dev Release</Configurations>
 		<Platforms>AnyCPU;x64;x86</Platforms>
 		<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
+        <ImplicitUsings>true</ImplicitUsings>
 	</PropertyGroup>
 
 	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='MSIX|AnyCPU'">

+ 6 - 10
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -16,6 +16,8 @@ using System.IO;
 using System.Linq;
 using System.Windows;
 using System.Windows.Media.Imaging;
+using PixiEditor.Models.Commands.Attributes;
+using System.Windows.Input;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -51,7 +53,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             : base(owner)
         {
             OpenNewFilePopupCommand = new RelayCommand(OpenNewFilePopup);
-            SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
+            //SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
             OpenFileCommand = new RelayCommand(Open);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
             OpenRecentCommand = new RelayCommand(OpenRecent);
@@ -152,11 +154,6 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
         }
 
-        public void SaveDocument(bool asNew)
-        {
-            SaveDocument(parameter: asNew ? "asnew" : null);
-        }
-
         public void OpenAny()
         {
             Open((object)null);
@@ -243,11 +240,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
         }
 
-        private void SaveDocument(object parameter)
+        [Command.Basic("PixiEditor.File.Save", false, "Save", "Save image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control)]
+        public void SaveDocument(bool asNew)
         {
-            bool paramIsAsNew = parameter != null && parameter.ToString()?.ToLower() == "asnew";
-            if (paramIsAsNew ||
-                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath)) 
+            if (asNew || string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath)) 
             {
                 Owner.BitmapManager.ActiveDocument.SaveWithDialog();
             }

+ 23 - 0
PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs

@@ -107,6 +107,29 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
 
             Owner.ShortcutController.KeyPressed(key, Keyboard.Modifiers);
+
+            //public void KeyPressed(Key key, ModifierKeys modifiers)
+            //{
+            //    if (!ShortcutExecutionBlocked)
+            //    {
+            //        Shortcut[] shortcuts = ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().FindAll(x => x.ShortcutKey == key).ToArray();
+            //        if (shortcuts.Length < 1)
+            //        {
+            //            return;
+            //        }
+
+            //        shortcuts = shortcuts.OrderByDescending(x => x.Modifier).ToArray();
+            //        for (int i = 0; i < shortcuts.Length; i++)
+            //        {
+            //            if (modifiers.HasFlag(shortcuts[i].Modifier))
+            //            {
+            //                shortcuts[i].Execute();
+            //                LastShortcut = shortcuts[i];
+            //                break;
+            //            }
+            //        }
+            //    }
+            //}
         }
 
         private void OnKeyUp(KeyEventArgs args)

+ 5 - 0
PixiEditor/ViewModels/ViewModelMain.cs

@@ -8,6 +8,7 @@ using System.Windows.Input;
 using System.Windows.Threading;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers.Shortcuts;
 using PixiEditor.Models.DataHolders;
@@ -70,6 +71,8 @@ namespace PixiEditor.ViewModels
 
         public BitmapManager BitmapManager { get; set; }
 
+        public CommandController CommandController { get; set; }
+
         public ShortcutController ShortcutController { get; set; }
 
         public StylusViewModel StylusSubViewModel { get; set; }
@@ -223,6 +226,8 @@ namespace PixiEditor.ViewModels
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
 
             ToolsSubViewModel?.SetupToolsTooltipShortcuts(services);
+
+            CommandController = services.GetService<CommandController>();
         }
 
         /// <summary>

+ 2 - 2
PixiEditor/Views/MainWindow.xaml

@@ -17,7 +17,7 @@
         xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" 
         xmlns:avalonDockTheme="clr-namespace:PixiEditor.Styles.AvalonDock" 
         xmlns:layerUserControls="clr-namespace:PixiEditor.Views.UserControls.Layers" 
-        xmlns:sys="clr-namespace:System;assembly=System.Runtime" 
+        xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
         d:DataContext="{d:DesignInstance Type=vm:ViewModelMain}"
         mc:Ignorable="d" WindowStyle="None" Initialized="MainWindow_Initialized"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
@@ -99,7 +99,7 @@
                             </DataTemplate>
                         </MenuItem.ItemTemplate>
                     </MenuItem>
-                    <MenuItem Header="_Save" InputGestureText="Ctrl+S" Command="{Binding FileSubViewModel.SaveDocumentCommand}" />
+                    <MenuItem Header="_Save" InputGestureText="{cmds:ShortcutBinding PixiEditor.File.Save}" Command="{cmds:Command PixiEditor.File.Save}" />
                     <MenuItem Header="_Save As..." InputGestureText="Ctrl+Shift+S"
                               Command="{Binding FileSubViewModel.SaveDocumentCommand}" CommandParameter="AsNew" />
                     <MenuItem Header="_Export" InputGestureText="Ctrl+Shift+Alt+S" Command="{Binding FileSubViewModel.ExportFileCommand}" />

+ 1 - 3
PixiEditor/Views/UserControls/NumberInput.xaml.cs

@@ -1,9 +1,7 @@
-using System;
-using System.Text.RegularExpressions;
+using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
-using PixiEditor.Models.Controllers.Shortcuts;
 
 namespace PixiEditor.Views
 {