Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,41 @@ 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)) {
MethodInfo? getter = property.GetGetMethod ();
if (getter is not null) {
return (Func<bool>) getter.CreateDelegate (typeof (Func<bool>), 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<bool>) method.CreateDelegate (typeof (Func<bool>), source);
}

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