diff --git a/CHANGELOG.md b/CHANGELOG.md index 673c8d6d42..8112598fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,13 @@ Thanks to the following contributors who worked on this release: - @cameronwhite - @Lehonti - @spaghetti22 +- @Matthieu-LAURENT39 ### Added - The splatter brush now allows the minimum and maximum splatter size to be configured separately from the brush width ### Changed +- Effect dialogs now hide options that are not currently relevant (#1960) ### Fixed - Fixed a bug where duplicate submenus could be produced by add-ins with effect categories that were not translated (#1933, #1935) diff --git a/Pinta.Core/Classes/DialogAttributes.cs b/Pinta.Core/Classes/DialogAttributes.cs index b5c2e87056..fb321e3a8d 100644 --- a/Pinta.Core/Classes/DialogAttributes.cs +++ b/Pinta.Core/Classes/DialogAttributes.cs @@ -75,3 +75,28 @@ public sealed class StaticListAttribute : Attribute public string DictionaryName { get; set; } } + +/// +/// Attribute for controlling the visibility of a control based on a condition. +/// The control will be hidden when the condition evaluates to false. +/// +[AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public sealed class VisibleWhenAttribute : Attribute +{ + public VisibleWhenAttribute (string conditionMethodName) => ConditionMethodName = conditionMethodName; + + public string ConditionMethodName { get; } +} + +/// +/// Attribute for controlling the enabled state of a control based on a condition. +/// The control will be disabled when the condition evaluates to false. +/// +[AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public sealed class EnabledWhenAttribute : Attribute +{ + public EnabledWhenAttribute (string conditionMethodName) => ConditionMethodName = conditionMethodName; + + public string ConditionMethodName { get; } +} + diff --git a/Pinta.Effects/Effects/CellsEffect.cs b/Pinta.Effects/Effects/CellsEffect.cs index 5b810dcc01..43a16ab3bf 100644 --- a/Pinta.Effects/Effects/CellsEffect.cs +++ b/Pinta.Effects/Effects/CellsEffect.cs @@ -181,9 +181,11 @@ public sealed class CellsData : EffectData public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.PresetGradient; [Caption ("Color Scheme")] + [VisibleWhen (nameof (ShowColorScheme))] public PresetGradients ColorScheme { get; set; } = PresetGradients.BlackAndWhite; [Caption ("Random Color Scheme Seed")] + [VisibleWhen (nameof (ShowColorSchemeSeed))] public RandomSeed ColorSchemeSeed { get; set; } = new (0); [Caption ("Reverse Color Scheme")] @@ -195,5 +197,11 @@ public sealed class CellsData : EffectData [Caption ("Quality")] [MinimumValue (1), MaximumValue (4)] public int Quality { get; set; } = 3; + + [Skip] + public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient; + + [Skip] + public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random; } } diff --git a/Pinta.Effects/Effects/CloudsEffect.cs b/Pinta.Effects/Effects/CloudsEffect.cs index 6fb5d6a0ee..cd55d70c39 100644 --- a/Pinta.Effects/Effects/CloudsEffect.cs +++ b/Pinta.Effects/Effects/CloudsEffect.cs @@ -146,12 +146,20 @@ public sealed class CloudsData : EffectData public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.SelectedColors; [Caption ("Color Scheme")] + [VisibleWhen (nameof (ShowColorScheme))] public PresetGradients ColorScheme { get; set; } = PresetGradients.BeautifulItaly; [Caption ("Random Color Scheme Seed")] + [VisibleWhen (nameof (ShowColorSchemeSeed))] public RandomSeed ColorSchemeSeed { get; set; } = new (0); [Caption ("Reverse Color Scheme")] public bool ReverseColorScheme { get; set; } = false; + + [Skip] + public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient; + + [Skip] + public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random; } } diff --git a/Pinta.Effects/Effects/JuliaFractalEffect.cs b/Pinta.Effects/Effects/JuliaFractalEffect.cs index ee25721a8f..7b0fbdd3e4 100644 --- a/Pinta.Effects/Effects/JuliaFractalEffect.cs +++ b/Pinta.Effects/Effects/JuliaFractalEffect.cs @@ -156,9 +156,11 @@ public sealed class JuliaFractalData : EffectData public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.PresetGradient; [Caption ("Color Scheme")] + [VisibleWhen (nameof (ShowColorScheme))] public PresetGradients ColorScheme { get; set; } = PresetGradients.Bonfire; [Caption ("Random Color Scheme Seed")] + [VisibleWhen (nameof (ShowColorSchemeSeed))] public RandomSeed ColorSchemeSeed { get; set; } = new (0); [Caption ("Reverse Color Scheme")] @@ -166,5 +168,11 @@ public sealed class JuliaFractalData : EffectData [Caption ("Angle")] public DegreesAngle Angle { get; set; } = new (0); + + [Skip] + public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient; + + [Skip] + public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random; } } diff --git a/Pinta.Effects/Effects/MandelbrotFractalEffect.cs b/Pinta.Effects/Effects/MandelbrotFractalEffect.cs index eaa387cd68..8f3d825960 100644 --- a/Pinta.Effects/Effects/MandelbrotFractalEffect.cs +++ b/Pinta.Effects/Effects/MandelbrotFractalEffect.cs @@ -172,9 +172,11 @@ public sealed class MandelbrotFractalData : EffectData public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.PresetGradient; [Caption ("Color Scheme")] + [VisibleWhen (nameof (ShowColorScheme))] public PresetGradients ColorScheme { get; set; } = PresetGradients.Electric; [Caption ("Random Color Scheme Seed")] + [VisibleWhen (nameof (ShowColorSchemeSeed))] public RandomSeed ColorSchemeSeed { get; set; } = new (0); [Caption ("Reverse Color Scheme")] @@ -182,5 +184,11 @@ public sealed class MandelbrotFractalData : EffectData [Caption ("Invert Colors")] public bool InvertColors { get; set; } = false; + + [Skip] + public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient; + + [Skip] + public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random; } } diff --git a/Pinta.Effects/Effects/VoronoiDiagramEffect.cs b/Pinta.Effects/Effects/VoronoiDiagramEffect.cs index 0fb9d2ba56..b94e74a0e8 100644 --- a/Pinta.Effects/Effects/VoronoiDiagramEffect.cs +++ b/Pinta.Effects/Effects/VoronoiDiagramEffect.cs @@ -202,6 +202,7 @@ public sealed class VoronoiDiagramData : EffectData public PointArrangement PointArrangement { get; set; } = PointArrangement.Random; [Caption ("Random Point Locations")] + [VisibleWhen (nameof (ShowRandomPointLocation))] public RandomSeed RandomPointLocations { get; set; } = new (0); [Caption ("Show Points")] @@ -209,14 +210,22 @@ public sealed class VoronoiDiagramData : EffectData [Caption ("Point Size")] [MinimumValue (1), MaximumValue (16), IncrementValue (1)] + [VisibleWhen (nameof (ShowPointConfig))] public double PointSize { get; set; } = 4; [Caption ("Point Color")] + [VisibleWhen (nameof (ShowPointConfig))] public Color PointColor { get; set; } = Color.Black; [Caption ("Quality")] [MinimumValue (1), MaximumValue (4)] public int Quality { get; set; } = 3; + + [Skip] + public bool ShowRandomPointLocation => PointArrangement == PointArrangement.Random; + + [Skip] + public bool ShowPointConfig => ShowPoints; } public enum ColorSorting diff --git a/Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs b/Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs index b9b73fb744..29f8428108 100644 --- a/Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs +++ b/Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs @@ -20,4 +20,41 @@ internal static class ReflectionHelper else throw new ArgumentException ($"Member \'{name}\' does not exist"); } + + /// + /// Evaluates a condition from a property or method of an object. + /// + /// The object containing the property or method. + /// The name of the property or method to evaluate. + /// The boolean result of the property or method. + public static bool EvaluateCondition (object source, string methodName) + => CreateConditionDelegate (source, methodName) (); + + /// + /// Creates a delegate for evaluating a condition from a property or method of an object. + /// This avoids needing to redo reflection lookups when the condition needs to be evaluated frequently. + /// + /// The object containing the property or method. + /// The name of the property or method to evaluate. + /// A delegate that evaluates the condition, giving the boolean result of the property or method. + public static Func CreateConditionDelegate (object source, string methodName) + { + Type type = source.GetType (); + + // Try to find a property first + PropertyInfo? property = type.GetProperty (methodName, BindingFlags.Public | BindingFlags.Instance); + if (property is not null && property.PropertyType == typeof (bool)) { + MethodInfo? getter = property.GetGetMethod (); + if (getter is not null) { + return (Func) getter.CreateDelegate (typeof (Func), source); + } + } + // If we couldn't find a property, try to find a method + MethodInfo? method = type.GetMethod (methodName, BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); + if (method is not null && method.ReturnType == typeof (bool)) { + return (Func) method.CreateDelegate (typeof (Func), source); + } + + throw new ArgumentException ($"Member \'{methodName}\' is not a boolean property or method"); + } } diff --git a/Pinta.Gui.Widgets/Dialogs/SimpleEffectDialog.cs b/Pinta.Gui.Widgets/Dialogs/SimpleEffectDialog.cs index e586ee5342..2afc93b99b 100644 --- a/Pinta.Gui.Widgets/Dialogs/SimpleEffectDialog.cs +++ b/Pinta.Gui.Widgets/Dialogs/SimpleEffectDialog.cs @@ -48,6 +48,17 @@ public sealed class SimpleEffectDialog : Gtk.Dialog private delegate bool TimeoutHandler (); TimeoutHandler? timeout_func; + private readonly EffectData effect_data; + + // Track widgets with conditional visibility/enabled, so we can update + // them when the effect data changes + private sealed record ConditionalWidget ( + Gtk.Widget Widget, + Func? VisibleWhenDelegate, + Func? EnabledWhenDelegate + ); + private readonly List conditional_widgets = new (); + /// Since this dialog is used by add-ins, the IAddinLocalizer allows for translations to be /// fetched from the appropriate place. /// @@ -82,6 +93,9 @@ public SimpleEffectDialog ( contentAreaBox.Append (widget); OnClose += (_, _) => HandleClose (); + + // Keep reference to effect data, so it can be used for handling conditional widgets + effect_data = effectData; } /// @@ -132,8 +146,8 @@ private void HandleClose () .GetMembers () .Where (IsInstanceFieldOrProperty) .Where (IsCustomProperty) + .Where (member => !member.GetCustomAttributes (false).Any ()) .Select (CreateSettings) - .Where (settings => !settings.skip) .Select (settings => GenerateWidgetsForMember (settings, effectData, localizer, workspace)) .SelectMany (widgets => widgets); @@ -160,7 +174,9 @@ private sealed record MemberSettings ( MemberReflector reflector, string caption, string? hint, - bool skip); + string? visibleWhenMethodName, + string? enabledWhenMethodName + ); private static MemberSettings CreateSettings (MemberInfo memberInfo) { @@ -172,11 +188,24 @@ private static MemberSettings CreateSettings (MemberInfo memberInfo) .Select (h => h.Caption) .FirstOrDefault (); + string? visibleConditionMethodName = + reflector.Attributes + .OfType () + .Select (v => v.ConditionMethodName) + .FirstOrDefault (); + + string? enabledConditionMethodName = + reflector.Attributes + .OfType () + .Select (e => e.ConditionMethodName) + .FirstOrDefault (); + return new ( reflector: reflector, caption: caption ?? MakeCaption (memberInfo.Name), hint: reflector.Attributes.OfType ().Select (h => h.Hint).FirstOrDefault (), - skip: reflector.Attributes.OfType ().Any ()); + visibleWhenMethodName: visibleConditionMethodName, + enabledWhenMethodName: enabledConditionMethodName); } private static string MakeCaption (string name) @@ -209,6 +238,19 @@ static IEnumerable GenerateCharacters (string name) } } + /// + /// Updates all widgets with conditional attributes. + /// + private void UpdateConditionalWidgets (EffectData effectData) + { + foreach (var widget in conditional_widgets) { + if (widget.VisibleWhenDelegate is not null) + widget.Widget.Visible = widget.VisibleWhenDelegate (); + + if (widget.EnabledWhenDelegate is not null) + widget.Widget.Sensitive = widget.EnabledWhenDelegate (); + } + } private IEnumerable GenerateWidgetsForMember ( MemberSettings settings, EffectData effectData, @@ -217,8 +259,36 @@ static IEnumerable GenerateCharacters (string name) { WidgetFactory? widgetFactory = GetWidgetFactory (settings); - if (widgetFactory is not null) - yield return widgetFactory (localizer.GetString (settings.caption), effectData, settings, workspace); + if (widgetFactory is not null) { + Gtk.Widget widget = widgetFactory (localizer.GetString (settings.caption), effectData, settings, workspace); + + // Keep a reference to widget if it has conditional attributes so we can update it later + if (settings.visibleWhenMethodName is not null || settings.enabledWhenMethodName is not null) { + + // Create delegates for condition evaluation (do reflection once, not on every update) + Func? visibleDelegate = settings.visibleWhenMethodName is not null + ? ReflectionHelper.CreateConditionDelegate (effectData, settings.visibleWhenMethodName) + : null; + + Func? enabledDelegate = settings.enabledWhenMethodName is not null + ? ReflectionHelper.CreateConditionDelegate (effectData, settings.enabledWhenMethodName) + : null; + + // Apply the initial state + if (visibleDelegate is not null) + widget.Visible = visibleDelegate (); + + if (enabledDelegate is not null) + widget.Sensitive = enabledDelegate (); + + conditional_widgets.Add (new ConditionalWidget ( + widget, + visibleDelegate, + enabledDelegate) + ); + } + yield return widget; + } if (settings.hint != null) yield return CreateHintLabel (localizer.GetString (settings.hint)); @@ -530,6 +600,9 @@ private void SetAndNotify (MemberReflector reflector, object o, object val) { reflector.SetValue (o, val); EffectDataChanged?.Invoke (this, new PropertyChangedEventArgs (reflector.OriginalMemberInfo.Name)); + + // Update conditional widgets when any property changes + UpdateConditionalWidgets (effect_data); } private Gtk.Widget CreateSeed (