Browse Source

Working CanExecuteChanged implementation

Krzysztof Krysiński 1 year ago
parent
commit
b3ffaca1da

+ 4 - 4
src/PixiEditor.AvaloniaUI/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs

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

+ 27 - 13
src/PixiEditor.AvaloniaUI/Models/Commands/CommandController.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
 using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
 using PixiEditor.AvaloniaUI.Helpers.Extensions;
@@ -41,6 +42,8 @@ internal class CommandController
 
     public Dictionary<string, IconEvaluator> IconEvaluators { get; }
 
+    private static readonly List<Command> objectsToInvokeOn = new();
+    
     public CommandController()
     {
         Current ??= this;
@@ -129,7 +132,7 @@ internal class CommandController
         LoadEvaluators(serviceProvider, compiledCommandList);
         LoadCommands(serviceProvider, compiledCommandList, commandGroupsData, commands, template);
         LoadTools(serviceProvider, commandGroupsData, commands, template);
-
+        
         var miscList = new List<Command>();
 
         foreach (var (groupInternalName, storedCommands) in commands)
@@ -148,6 +151,27 @@ internal class CommandController
         CommandGroups.Add(new CommandGroup("MISC", miscList));
     }
 
+    public static void ListenForCanExecuteChanged(Command command)
+    {
+        objectsToInvokeOn.Add(command);
+    }
+
+    public static void StopListeningForCanExecuteChanged(Command handler)
+    {
+        objectsToInvokeOn.Remove(handler);
+    }
+
+    public void NotifyPropertyChanged(string? propertyName)
+    {
+        foreach (var evaluator in objectsToInvokeOn)
+        {
+            if (evaluator.Methods.CanExecuteEvaluator.DependentOn != null && evaluator.Methods.CanExecuteEvaluator.DependentOn.Contains(propertyName))
+            {
+                evaluator.OnCanExecuteChanged();
+            }
+        }
+    }
+
     private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, LocalizedString displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,
         ShortcutsTemplate template)
     {
@@ -414,8 +438,6 @@ internal class CommandController
                 T evaluator = factory(x => Task.Run(async () => await func(x)).Result);//TODO: This is not truly async
                 evaluators.Add(evaluator.Name, evaluator);
             }
-
-
         }
 
         void AddEvaluator<TAttr, T, TParameter>(MethodInfo method, object instance, TAttr attribute,
@@ -442,11 +464,6 @@ internal class CommandController
                         {
                             case Evaluator.CanExecuteAttribute canExecuteAttribute:
                             {
-                                var getRequiredEvaluatorsObjectsOfCurrentEvaluator =
-                                    (CommandController controller) =>
-                                        canExecuteAttribute.NamesOfRequiredCanExecuteEvaluators.Select(x =>
-                                            controller.CanExecuteEvaluators[x]);
-
                                 AddEvaluatorFactory<Evaluator.CanExecuteAttribute, CanExecuteEvaluator, bool>(
                                     methodInfo,
                                     serviceProvider.GetService(type.Key),
@@ -455,11 +472,8 @@ internal class CommandController
                                     evaluateFunction => new CanExecuteEvaluator()
                                     {
                                         Name = attribute.Name,
-                                        Evaluate = evaluateFunctionArgument =>
-                                            evaluateFunction.Invoke(evaluateFunctionArgument) &&
-                                            getRequiredEvaluatorsObjectsOfCurrentEvaluator.Invoke(this).All(
-                                                requiredEvaluator =>
-                                                    requiredEvaluator.CallEvaluate(null, evaluateFunctionArgument)),
+                                        Evaluate = evaluateFunction.Invoke,
+                                        DependentOn = canExecuteAttribute.DependentOn
                                     });
                                 break;
                             }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/Commands/CommandMethods.cs

@@ -6,6 +6,7 @@ namespace PixiEditor.AvaloniaUI.Models.Commands;
 
 internal class CommandMethods
 {
+    public CanExecuteEvaluator CanExecuteEvaluator => _canExecute;
     private readonly Command _command;
     private readonly Action<object> _execute;
     private readonly CanExecuteEvaluator _canExecute;

+ 8 - 1
src/PixiEditor.AvaloniaUI/Models/Commands/Commands/Command.cs

@@ -4,12 +4,13 @@ using Avalonia.Media;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.AvaloniaUI.Models.Commands.Evaluators;
 using PixiEditor.AvaloniaUI.Models.Input;
+using PixiEditor.AvaloniaUI.ViewModels;
 using PixiEditor.Extensions.Common.Localization;
 
 namespace PixiEditor.AvaloniaUI.Models.Commands.Commands;
 
 [DebuggerDisplay("{InternalName,nq} ('{DisplayName,nq}')")]
-internal abstract partial class Command : ObservableObject
+internal abstract partial class Command : PixiObservableObject
 {
     private KeyCombination _shortcut;
 
@@ -43,6 +44,7 @@ internal abstract partial class Command : ObservableObject
     }
 
     public event ShortcutChangedEventHandler ShortcutChanged;
+    public event Action CanExecuteChanged;
 
     public abstract object GetParameter();
 
@@ -69,4 +71,9 @@ internal abstract partial class Command : ObservableObject
     public IImage GetIcon() => IconEvaluator == null ? null : IconEvaluator.CallEvaluate(this, GetParameter());
 
     public delegate void ShortcutChangedEventHandler(Command command, ShortcutChangedEventArgs args);
+
+    public void OnCanExecuteChanged()
+    {
+        CanExecuteChanged?.Invoke();
+    }
 }

+ 4 - 1
src/PixiEditor.AvaloniaUI/Models/Commands/Evaluators/CanExecuteEvaluator.cs

@@ -1,4 +1,6 @@
-using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading.Tasks;
 using PixiEditor.AvaloniaUI.Models.Commands.Commands;
 
 namespace PixiEditor.AvaloniaUI.Models.Commands.Evaluators;
@@ -8,6 +10,7 @@ internal class CanExecuteEvaluator : Evaluator<bool>
     public static CanExecuteEvaluator AlwaysTrue { get; } = new StaticValueEvaluator(true);
 
     public static CanExecuteEvaluator AlwaysFalse { get; } = new StaticValueEvaluator(false);
+    public string[]? DependentOn { get; set; }
 
     private class StaticValueEvaluator : CanExecuteEvaluator
     {

+ 36 - 9
src/PixiEditor.AvaloniaUI/Models/Commands/XAML/Command.cs

@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Threading.Tasks;
 using System.Windows.Input;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
@@ -53,14 +54,40 @@ internal class Command : MarkupExtension
 
     class ProvidedICommand : ICommand
     {
-        //TODO: Not found in Avalonia
-        public event EventHandler? CanExecuteChanged;
-        /* {
-             add => CommandManager.RequerySuggested += value;
-             remove => CommandManager.RequerySuggested -= value;
-         }*/
-
-        public Commands.Command Command { get; init; }
+        public event EventHandler? CanExecuteChanged
+        {
+            add
+            {
+                if (CanExecuteChangedHandlers.Count == 0)
+                {
+                    CommandController.ListenForCanExecuteChanged(Command);
+                }
+
+                CanExecuteChangedHandlers.Add(value);
+            }
+            remove
+            {
+                CanExecuteChangedHandlers.Remove(value);
+                if (CanExecuteChangedHandlers.Count == 0)
+                {
+                    CommandController.StopListeningForCanExecuteChanged(Command);
+                }
+            }
+        }
+
+        private List<EventHandler> CanExecuteChangedHandlers { get; } = new();
+
+        private Commands.Command command;
+
+        public Commands.Command Command
+        {
+            get => command;
+            init
+            {
+                command = value;
+                Command.CanExecuteChanged += () => CanExecuteChangedHandlers.ForEach(x => x.Invoke(this, EventArgs.Empty));
+            }
+        }
 
         public bool UseProvidedParameter { get; init; }
 

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentManagerViewModel.cs

@@ -56,7 +56,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
 
     public void MakeActiveDocumentNull() => ActiveDocument = null;
 
-    [Evaluator.CanExecute("PixiEditor.HasDocument")]
+    [Evaluator.CanExecute("PixiEditor.HasDocument", nameof(ActiveDocument))]
     public bool DocumentNotNull() => ActiveDocument != null;
 
     [Command.Basic("PixiEditor.Document.ClipCanvas", "CLIP_CANVAS", "CLIP_CANVAS", CanExecute = "PixiEditor.HasDocument", IconPath = "crop.png")]

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -39,7 +39,7 @@ using Point = Avalonia.Point;
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
 #nullable enable
-internal partial class DocumentViewModel : ObservableObject, IDocument
+internal partial class DocumentViewModel : PixiObservableObject, IDocument
 {
     public event EventHandler<LayersChangedEventArgs>? LayersChanged;
     public event EventHandler<DocumentSizeChangedEventArgs>? SizeChanged;

+ 14 - 0
src/PixiEditor.AvaloniaUI/ViewModels/PixiObservableObject.cs

@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.Commands;
+
+namespace PixiEditor.AvaloniaUI.ViewModels;
+
+public class PixiObservableObject : ObservableObject
+{
+    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+    {
+        base.OnPropertyChanged(e);
+        CommandController.Current.NotifyPropertyChanged(e.PropertyName);
+    }
+}

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/ViewModelBase.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.AvaloniaUI.ViewModels;
 
-public class ViewModelBase : ObservableObject
+public class ViewModelBase : PixiObservableObject
 {
     public void AddPropertyChangedCallback(string propertyName, Action action)
     {