CommandController.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. using Microsoft.Extensions.DependencyInjection;
  2. using PixiEditor.Models.Commands.Attributes;
  3. using PixiEditor.Models.Commands.Evaluators;
  4. using PixiEditor.Models.DataHolders;
  5. using PixiEditor.Models.Tools;
  6. using System.IO;
  7. using System.Reflection;
  8. using System.Windows.Media;
  9. using CommandAttribute = PixiEditor.Models.Commands.Attributes.Command;
  10. namespace PixiEditor.Models.Commands
  11. {
  12. public class CommandController
  13. {
  14. private readonly ShortcutFile shortcutFile;
  15. public static CommandController Current { get; private set; }
  16. public static string ShortcutsPath { get; private set; }
  17. public CommandCollection Commands { get; }
  18. public List<CommandGroup> CommandGroups { get; }
  19. public Dictionary<string, CanExecuteEvaluator> CanExecuteEvaluators { get; }
  20. public Dictionary<string, IconEvaluator> IconEvaluators { get; }
  21. public CommandController(IServiceProvider services)
  22. {
  23. Current ??= this;
  24. ShortcutsPath = Path.Join(
  25. Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
  26. "PixiEditor",
  27. "shortcuts.json");
  28. shortcutFile = new(ShortcutsPath, this);
  29. Commands = new();
  30. CommandGroups = new();
  31. CanExecuteEvaluators = new();
  32. IconEvaluators = new();
  33. }
  34. public void Import(IEnumerable<KeyValuePair<KeyCombination, IEnumerable<string>>> shortcuts, bool save = true)
  35. {
  36. foreach (var shortcut in shortcuts)
  37. {
  38. foreach (var command in shortcut.Value)
  39. {
  40. ReplaceShortcut(Commands[command], shortcut.Key);
  41. }
  42. }
  43. if (save)
  44. {
  45. shortcutFile.SaveShortcuts();
  46. }
  47. }
  48. private static List<(string internalName, string displayName)> FindCommandGroups(Type[] typesToSearchForAttributes)
  49. {
  50. List<(string internalName, string displayName)> result = new();
  51. foreach (var type in typesToSearchForAttributes)
  52. {
  53. foreach (var group in type.GetCustomAttributes<CommandAttribute.GroupAttribute>())
  54. {
  55. result.Add((group.InternalName, group.DisplayName));
  56. }
  57. }
  58. return result;
  59. }
  60. private static void ForEachMethod
  61. (Type[] typesToSearchForMethods, IServiceProvider serviceProvider, Action<MethodInfo, object> action)
  62. {
  63. foreach (var type in typesToSearchForMethods)
  64. {
  65. object serviceInstance = serviceProvider.GetService(type);
  66. var methods = type.GetMethods();
  67. foreach (var method in methods)
  68. {
  69. action(method, serviceInstance);
  70. }
  71. }
  72. }
  73. public void Init(IServiceProvider serviceProvider)
  74. {
  75. KeyValuePair<KeyCombination, IEnumerable<string>>[] shortcuts = shortcutFile.LoadShortcuts()?.ToArray()
  76. ?? Array.Empty<KeyValuePair<KeyCombination, IEnumerable<string>>>();
  77. Type[] allTypesInPixiEditorAssembly = typeof(CommandController).Assembly.GetTypes();
  78. List<(string internalName, string displayName)> commandGroupsData = FindCommandGroups(allTypesInPixiEditorAssembly);
  79. OneToManyDictionary<string, Command> commands = new(); // internal name of the corr. group -> command in that group
  80. // Find evaluators
  81. ForEachMethod(allTypesInPixiEditorAssembly, serviceProvider, (methodInfo, maybeServiceInstance) =>
  82. {
  83. var evaluatorAttrs = methodInfo.GetCustomAttributes<Evaluator.EvaluatorAttribute>();
  84. foreach (var attribute in evaluatorAttrs)
  85. {
  86. switch (attribute)
  87. {
  88. case Evaluator.CanExecuteAttribute canExecuteAttribute:
  89. {
  90. var getRequiredEvaluatorsObjectsOfCurrentEvaluator =
  91. (CommandController controller) =>
  92. canExecuteAttribute.NamesOfRequiredCanExecuteEvaluators.Select(x => controller.CanExecuteEvaluators[x]);
  93. AddEvaluatorFactory<Evaluator.CanExecuteAttribute, CanExecuteEvaluator, bool>(
  94. methodInfo,
  95. maybeServiceInstance,
  96. canExecuteAttribute,
  97. CanExecuteEvaluators,
  98. evaluateFunction => new CanExecuteEvaluator()
  99. {
  100. Name = attribute.Name,
  101. Evaluate = evaluateFunctionArgument =>
  102. evaluateFunction.Invoke(evaluateFunctionArgument) &&
  103. getRequiredEvaluatorsObjectsOfCurrentEvaluator.Invoke(this).All(requiredEvaluator =>
  104. requiredEvaluator.CallEvaluate(null, evaluateFunctionArgument))
  105. });
  106. break;
  107. }
  108. case Evaluator.IconAttribute icon:
  109. AddEvaluator<Evaluator.IconAttribute, IconEvaluator, ImageSource>(methodInfo, maybeServiceInstance, icon, IconEvaluators);
  110. break;
  111. }
  112. }
  113. });
  114. // Find basic commands
  115. ForEachMethod(allTypesInPixiEditorAssembly, serviceProvider, (methodInfo, maybeServiceInstance) =>
  116. {
  117. var commandAttrs = methodInfo.GetCustomAttributes<CommandAttribute.CommandAttribute>();
  118. foreach (var attribute in commandAttrs)
  119. {
  120. if (attribute is CommandAttribute.BasicAttribute basic)
  121. {
  122. AddCommand(methodInfo, maybeServiceInstance, attribute, (isDebug, name, x, xCan, xIcon) => new Command.BasicCommand(x, xCan)
  123. {
  124. InternalName = name,
  125. IsDebug = isDebug,
  126. DisplayName = attribute.DisplayName,
  127. Description = attribute.Description,
  128. IconPath = attribute.IconPath,
  129. IconEvaluator = xIcon,
  130. DefaultShortcut = attribute.GetShortcut(),
  131. Shortcut = GetShortcut(name, attribute.GetShortcut()),
  132. Parameter = basic.Parameter,
  133. });
  134. }
  135. }
  136. });
  137. // Find tool commands
  138. foreach (var type in allTypesInPixiEditorAssembly)
  139. {
  140. if (!type.IsAssignableTo(typeof(Tool)))
  141. continue;
  142. var toolAttr = type.GetCustomAttribute<CommandAttribute.ToolAttribute>();
  143. if (toolAttr is null)
  144. continue;
  145. Tool toolInstance = serviceProvider.GetServices<Tool>().First(x => x.GetType() == type);
  146. string internalName = $"PixiEditor.Tools.Select.{type.Name}";
  147. var command = new Command.ToolCommand()
  148. {
  149. InternalName = internalName,
  150. DisplayName = $"Select {toolInstance.DisplayName} Tool",
  151. Description = $"Select {toolInstance.DisplayName} Tool",
  152. IconPath = $"@{toolInstance.ImagePath}",
  153. IconEvaluator = IconEvaluator.Default,
  154. TransientKey = toolAttr.Transient,
  155. DefaultShortcut = toolAttr.GetShortcut(),
  156. Shortcut = GetShortcut(internalName, toolAttr.GetShortcut()),
  157. ToolType = type,
  158. };
  159. Commands.Add(command);
  160. AddCommandToCommandsCollection(command);
  161. }
  162. // save all commands into CommandGroups
  163. foreach (var (groupInternalName, storedCommands) in commands)
  164. {
  165. var groupData = commandGroupsData.Where(group => group.internalName == groupInternalName).FirstOrDefault();
  166. string groupDisplayName;
  167. if (groupData == default)
  168. groupDisplayName = "Misc";
  169. else
  170. groupDisplayName = groupData.displayName;
  171. CommandGroups.Add(new(groupDisplayName, storedCommands));
  172. }
  173. KeyCombination GetShortcut(string internalName, KeyCombination defaultShortcut)
  174. => shortcuts.FirstOrDefault(x => x.Value.Contains(internalName), new(defaultShortcut, null)).Key;
  175. void AddCommandToCommandsCollection(Command command)
  176. {
  177. (string internalName, string displayName) group = commandGroupsData.FirstOrDefault(x => command.InternalName.StartsWith(x.internalName));
  178. if (group == default)
  179. commands.Add("", command);
  180. else
  181. commands.Add(group.internalName, command);
  182. }
  183. void AddEvaluator<TAttr, T, TParameter>(MethodInfo method, object instance, TAttr attribute, IDictionary<string, T> evaluators)
  184. where T : Evaluator<TParameter>, new()
  185. where TAttr : Evaluator.EvaluatorAttribute
  186. => AddEvaluatorFactory<TAttr, T, TParameter>(method, instance, attribute, evaluators, x => new T() { Name = attribute.Name, Evaluate = x });
  187. void AddEvaluatorFactory<TAttr, T, TParameter>(MethodInfo method, object serviceInstance, TAttr attribute, IDictionary<string, T> evaluators, Func<Func<object, TParameter>, T> factory)
  188. where T : Evaluator<TParameter>, new()
  189. where TAttr : Evaluator.EvaluatorAttribute
  190. {
  191. if (method.ReturnType != typeof(TParameter))
  192. {
  193. throw new Exception($"Invalid return type for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name}\nExpected '{typeof(TParameter).FullName}'");
  194. }
  195. else if (method.GetParameters().Length > 1)
  196. {
  197. throw new Exception($"Too many parameters for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name}");
  198. }
  199. else if (!method.IsStatic && serviceInstance is null)
  200. {
  201. throw new Exception($"No type instance for the CanExecute evaluator '{attribute.Name}' at {method.ReflectedType.FullName}.{method.Name} found");
  202. }
  203. var parameters = method.GetParameters();
  204. Func<object, TParameter> func;
  205. if (parameters.Length == 1)
  206. {
  207. func = x => (TParameter)method.Invoke(serviceInstance, new[] { CastParameter(x, parameters[0].ParameterType) });
  208. }
  209. else
  210. {
  211. func = x => (TParameter)method.Invoke(serviceInstance, null);
  212. }
  213. T evaluator = factory(func);
  214. evaluators.Add(evaluator.Name, evaluator);
  215. }
  216. object CastParameter(object input, Type target)
  217. {
  218. if (target == typeof(object) || target == input?.GetType())
  219. return input;
  220. return Convert.ChangeType(input, target);
  221. }
  222. TCommand AddCommand<TAttr, TCommand>(MethodInfo method, object instance, TAttr attribute, Func<bool, string, Action<object>, CanExecuteEvaluator, IconEvaluator, TCommand> commandFactory)
  223. where TAttr : CommandAttribute.CommandAttribute
  224. where TCommand : Command
  225. {
  226. if (method != null)
  227. {
  228. if (method.GetParameters().Length > 1)
  229. {
  230. throw new Exception($"Too many parameters for the CanExecute evaluator '{attribute.InternalName}' at {method.ReflectedType.FullName}.{method.Name}");
  231. }
  232. else if (!method.IsStatic && instance is null)
  233. {
  234. throw new Exception($"No type instance for the CanExecute evaluator '{attribute.InternalName}' at {method.ReflectedType.FullName}.{method.Name} found");
  235. }
  236. }
  237. var parameters = method?.GetParameters();
  238. Action<object> action;
  239. if (parameters == null || parameters.Length != 1)
  240. {
  241. action = x => method.Invoke(instance, null);
  242. }
  243. else
  244. {
  245. action = x => method.Invoke(instance, new[] { x });
  246. }
  247. string name = attribute.InternalName;
  248. bool isDebug = attribute.InternalName.StartsWith("#DEBUG#");
  249. if (attribute.InternalName.StartsWith("#DEBUG#"))
  250. {
  251. name = name["#DEBUG#".Length..];
  252. }
  253. var command = commandFactory(
  254. isDebug,
  255. name,
  256. action,
  257. attribute.CanExecute != null ? CanExecuteEvaluators[attribute.CanExecute] : CanExecuteEvaluator.AlwaysTrue,
  258. attribute.IconEvaluator != null ? IconEvaluators[attribute.IconEvaluator] : IconEvaluator.Default);
  259. Commands.Add(command);
  260. AddCommandToCommandsCollection(command);
  261. return command;
  262. }
  263. }
  264. /// <summary>
  265. /// Removes the old shortcut to this command and adds the new one
  266. /// </summary>
  267. public void UpdateShortcut(Command command, KeyCombination newShortcut)
  268. {
  269. Commands.RemoveShortcut(command, command.Shortcut);
  270. Commands.AddShortcut(command, newShortcut);
  271. command.Shortcut = newShortcut;
  272. shortcutFile.SaveShortcuts();
  273. }
  274. /// <summary>
  275. /// Deletes all shortcuts of <paramref name="newShortcut"/> and adds <paramref name="command"/>
  276. /// </summary>
  277. public void ReplaceShortcut(Command command, KeyCombination newShortcut)
  278. {
  279. foreach (Command other in Commands[newShortcut])
  280. {
  281. other.Shortcut = KeyCombination.None;
  282. }
  283. Commands.ClearShortcut(newShortcut);
  284. Commands.RemoveShortcut(command, command.Shortcut);
  285. Commands.AddShortcut(command, newShortcut);
  286. command.Shortcut = newShortcut;
  287. shortcutFile.SaveShortcuts();
  288. }
  289. public void ResetShortcuts()
  290. {
  291. File.Copy(ShortcutsPath, Path.ChangeExtension(ShortcutsPath, ".json.bak"), true);
  292. Commands.ClearShortcuts();
  293. foreach (var command in Commands)
  294. {
  295. Commands.RemoveShortcut(command, command.Shortcut);
  296. Commands.AddShortcut(command, command.DefaultShortcut);
  297. command.Shortcut = command.DefaultShortcut;
  298. }
  299. shortcutFile.SaveShortcuts();
  300. }
  301. }
  302. }