Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions Pinta.Core/Classes/DialogAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,28 @@ public sealed class StaticListAttribute : Attribute

public string DictionaryName { get; set; }
}

/// <summary>
/// Attribute for controlling the visibility of a control based on a condition.
/// The control will be hidden when the condition evaluates to false.
/// </summary>
[AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class VisibleWhenAttribute : Attribute
{
public VisibleWhenAttribute (string conditionMethodName) => ConditionMethodName = conditionMethodName;

public string ConditionMethodName { get; }
}

/// <summary>
/// Attribute for controlling the enabled state of a control based on a condition.
/// The control will be disabled when the condition evaluates to false.
/// </summary>
[AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class EnabledWhenAttribute : Attribute
{
public EnabledWhenAttribute (string conditionMethodName) => ConditionMethodName = conditionMethodName;

public string ConditionMethodName { get; }
}

8 changes: 8 additions & 0 deletions Pinta.Effects/Effects/CellsEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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;
}
}
8 changes: 8 additions & 0 deletions Pinta.Effects/Effects/CloudsEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
8 changes: 8 additions & 0 deletions Pinta.Effects/Effects/JuliaFractalEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,23 @@ 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")]
public bool ReverseColorScheme { get; set; } = false;

[Caption ("Angle")]
public DegreesAngle Angle { get; set; } = new (0);

[Skip]
public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient;

[Skip]
public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random;
}
}
8 changes: 8 additions & 0 deletions Pinta.Effects/Effects/MandelbrotFractalEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,23 @@ 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")]
public bool ReverseColorScheme { get; set; } = false;

[Caption ("Invert Colors")]
public bool InvertColors { get; set; } = false;

[Skip]
public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient;

[Skip]
public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random;
}
}
9 changes: 9 additions & 0 deletions Pinta.Effects/Effects/VoronoiDiagramEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,21 +202,30 @@ 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")]
public bool ShowPoints { get; set; } = false;

[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
Expand Down
34 changes: 34 additions & 0 deletions Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,38 @@ internal static class ReflectionHelper
else
throw new ArgumentException ($"Member \'{name}\' does not exist");
}

/// <summary>
/// Evaluates a condition from a property or method of an object.
/// </summary>
/// <param name="source">The object containing the property or method.</param>
/// <param name="methodName">The name of the property or method to evaluate.</param>
/// <returns>The boolean result of the property or method.</returns>
public static bool EvaluateCondition (object source, string methodName)
=> CreateConditionDelegate (source, methodName) ();

/// <summary>
/// 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.
/// </summary>
/// <param name="source">The object containing the property or method.</param>
/// <param name="methodName">The name of the property or method to evaluate.</param>
/// <returns>A delegate that evaluates the condition, giving the boolean result of the property or method.</returns>
public static Func<bool> 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)) {
return () => (bool) property.GetValue (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 () => (bool) method.Invoke (source, null)!;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This can actually go a step further - I should've mentioned this more clearly in my last comment :)

You can use MethodInfo.CreateDelegate () , which creates a strongly-typed delegate that's more like calling the method directly performance-wise

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, thanks!
I updated the implementation to use CreateDelegate

}

throw new ArgumentException ($"Member \'{methodName}\' is not a boolean property or method");
}
}
83 changes: 78 additions & 5 deletions Pinta.Gui.Widgets/Dialogs/SimpleEffectDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>? VisibleWhenDelegate,
Func<bool>? EnabledWhenDelegate
);
private readonly List<ConditionalWidget> conditional_widgets = new ();

/// Since this dialog is used by add-ins, the IAddinLocalizer allows for translations to be
/// fetched from the appropriate place.
/// </param>
Expand Down Expand Up @@ -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;
}

/// <summary>
Expand Down Expand Up @@ -132,8 +146,8 @@ private void HandleClose ()
.GetMembers ()
.Where (IsInstanceFieldOrProperty)
.Where (IsCustomProperty)
.Where (member => !member.GetCustomAttributes<SkipAttribute> (false).Any ())
.Select (CreateSettings)
.Where (settings => !settings.skip)
.Select (settings => GenerateWidgetsForMember (settings, effectData, localizer, workspace))
.SelectMany (widgets => widgets);

Expand All @@ -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)
{
Expand All @@ -172,11 +188,24 @@ private static MemberSettings CreateSettings (MemberInfo memberInfo)
.Select (h => h.Caption)
.FirstOrDefault ();

string? visibleConditionMethodName =
reflector.Attributes
.OfType<VisibleWhenAttribute> ()
.Select (v => v.ConditionMethodName)
.FirstOrDefault ();

string? enabledConditionMethodName =
reflector.Attributes
.OfType<EnabledWhenAttribute> ()
.Select (e => e.ConditionMethodName)
.FirstOrDefault ();

return new (
reflector: reflector,
caption: caption ?? MakeCaption (memberInfo.Name),
hint: reflector.Attributes.OfType<HintAttribute> ().Select (h => h.Hint).FirstOrDefault (),
skip: reflector.Attributes.OfType<SkipAttribute> ().Any ());
visibleWhenMethodName: visibleConditionMethodName,
enabledWhenMethodName: enabledConditionMethodName);
}

private static string MakeCaption (string name)
Expand Down Expand Up @@ -209,6 +238,19 @@ static IEnumerable<char> GenerateCharacters (string name)
}
}

/// <summary>
/// Updates all widgets with conditional attributes.
/// </summary>
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<Gtk.Widget> GenerateWidgetsForMember (
MemberSettings settings,
EffectData effectData,
Expand All @@ -217,8 +259,36 @@ static IEnumerable<char> 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<bool>? visibleDelegate = settings.visibleWhenMethodName is not null
? ReflectionHelper.CreateConditionDelegate (effectData, settings.visibleWhenMethodName)
: null;

Func<bool>? 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));
Expand Down Expand Up @@ -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 (
Expand Down
Loading