Skip to content

Commit ba09251

Browse files
Add VisibleWhen and EnabledWhen attributes to SimpleEffectDialog (#1960)
* add VisibleWhenAttribute and EnabledWhenAttribute to DialogAttributes This allows attributes in SimpleEffectDialog to automatically set their enabled state/visibility based on a condition * Retrofit effects with VisibleWhen
1 parent 6bb8148 commit ba09251

9 files changed

Lines changed: 183 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ Thanks to the following contributors who worked on this release:
77
- @cameronwhite
88
- @Lehonti
99
- @spaghetti22
10+
- @Matthieu-LAURENT39
1011

1112
### Added
1213
- The splatter brush now allows the minimum and maximum splatter size to be configured separately from the brush width
1314
- The status bar color palette now supports Ctrl+clicking to edit a color, in addition to middle clicking (#1436)
1415

1516
### Changed
17+
- Effect dialogs now hide options that are not currently relevant (#1960)
1618

1719
### Fixed
1820
- Fixed a bug where duplicate submenus could be produced by add-ins with effect categories that were not translated (#1933, #1935)

Pinta.Core/Classes/DialogAttributes.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,28 @@ public sealed class StaticListAttribute : Attribute
7575

7676
public string DictionaryName { get; set; }
7777
}
78+
79+
/// <summary>
80+
/// Attribute for controlling the visibility of a control based on a condition.
81+
/// The control will be hidden when the condition evaluates to false.
82+
/// </summary>
83+
[AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
84+
public sealed class VisibleWhenAttribute : Attribute
85+
{
86+
public VisibleWhenAttribute (string conditionMethodName) => ConditionMethodName = conditionMethodName;
87+
88+
public string ConditionMethodName { get; }
89+
}
90+
91+
/// <summary>
92+
/// Attribute for controlling the enabled state of a control based on a condition.
93+
/// The control will be disabled when the condition evaluates to false.
94+
/// </summary>
95+
[AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
96+
public sealed class EnabledWhenAttribute : Attribute
97+
{
98+
public EnabledWhenAttribute (string conditionMethodName) => ConditionMethodName = conditionMethodName;
99+
100+
public string ConditionMethodName { get; }
101+
}
102+

Pinta.Effects/Effects/CellsEffect.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,11 @@ public sealed class CellsData : EffectData
181181
public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.PresetGradient;
182182

183183
[Caption ("Color Scheme")]
184+
[VisibleWhen (nameof (ShowColorScheme))]
184185
public PresetGradients ColorScheme { get; set; } = PresetGradients.BlackAndWhite;
185186

186187
[Caption ("Random Color Scheme Seed")]
188+
[VisibleWhen (nameof (ShowColorSchemeSeed))]
187189
public RandomSeed ColorSchemeSeed { get; set; } = new (0);
188190

189191
[Caption ("Reverse Color Scheme")]
@@ -195,5 +197,11 @@ public sealed class CellsData : EffectData
195197
[Caption ("Quality")]
196198
[MinimumValue (1), MaximumValue (4)]
197199
public int Quality { get; set; } = 3;
200+
201+
[Skip]
202+
public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient;
203+
204+
[Skip]
205+
public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random;
198206
}
199207
}

Pinta.Effects/Effects/CloudsEffect.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,20 @@ public sealed class CloudsData : EffectData
146146
public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.SelectedColors;
147147

148148
[Caption ("Color Scheme")]
149+
[VisibleWhen (nameof (ShowColorScheme))]
149150
public PresetGradients ColorScheme { get; set; } = PresetGradients.BeautifulItaly;
150151

151152
[Caption ("Random Color Scheme Seed")]
153+
[VisibleWhen (nameof (ShowColorSchemeSeed))]
152154
public RandomSeed ColorSchemeSeed { get; set; } = new (0);
153155

154156
[Caption ("Reverse Color Scheme")]
155157
public bool ReverseColorScheme { get; set; } = false;
158+
159+
[Skip]
160+
public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient;
161+
162+
[Skip]
163+
public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random;
156164
}
157165
}

Pinta.Effects/Effects/JuliaFractalEffect.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,23 @@ public sealed class JuliaFractalData : EffectData
156156
public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.PresetGradient;
157157

158158
[Caption ("Color Scheme")]
159+
[VisibleWhen (nameof (ShowColorScheme))]
159160
public PresetGradients ColorScheme { get; set; } = PresetGradients.Bonfire;
160161

161162
[Caption ("Random Color Scheme Seed")]
163+
[VisibleWhen (nameof (ShowColorSchemeSeed))]
162164
public RandomSeed ColorSchemeSeed { get; set; } = new (0);
163165

164166
[Caption ("Reverse Color Scheme")]
165167
public bool ReverseColorScheme { get; set; } = false;
166168

167169
[Caption ("Angle")]
168170
public DegreesAngle Angle { get; set; } = new (0);
171+
172+
[Skip]
173+
public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient;
174+
175+
[Skip]
176+
public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random;
169177
}
170178
}

Pinta.Effects/Effects/MandelbrotFractalEffect.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,23 @@ public sealed class MandelbrotFractalData : EffectData
172172
public ColorSchemeSource ColorSchemeSource { get; set; } = ColorSchemeSource.PresetGradient;
173173

174174
[Caption ("Color Scheme")]
175+
[VisibleWhen (nameof (ShowColorScheme))]
175176
public PresetGradients ColorScheme { get; set; } = PresetGradients.Electric;
176177

177178
[Caption ("Random Color Scheme Seed")]
179+
[VisibleWhen (nameof (ShowColorSchemeSeed))]
178180
public RandomSeed ColorSchemeSeed { get; set; } = new (0);
179181

180182
[Caption ("Reverse Color Scheme")]
181183
public bool ReverseColorScheme { get; set; } = false;
182184

183185
[Caption ("Invert Colors")]
184186
public bool InvertColors { get; set; } = false;
187+
188+
[Skip]
189+
public bool ShowColorScheme => ColorSchemeSource == ColorSchemeSource.PresetGradient;
190+
191+
[Skip]
192+
public bool ShowColorSchemeSeed => ColorSchemeSource == ColorSchemeSource.Random;
185193
}
186194
}

Pinta.Effects/Effects/VoronoiDiagramEffect.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,21 +202,30 @@ public sealed class VoronoiDiagramData : EffectData
202202
public PointArrangement PointArrangement { get; set; } = PointArrangement.Random;
203203

204204
[Caption ("Random Point Locations")]
205+
[VisibleWhen (nameof (ShowRandomPointLocation))]
205206
public RandomSeed RandomPointLocations { get; set; } = new (0);
206207

207208
[Caption ("Show Points")]
208209
public bool ShowPoints { get; set; } = false;
209210

210211
[Caption ("Point Size")]
211212
[MinimumValue (1), MaximumValue (16), IncrementValue (1)]
213+
[VisibleWhen (nameof (ShowPointConfig))]
212214
public double PointSize { get; set; } = 4;
213215

214216
[Caption ("Point Color")]
217+
[VisibleWhen (nameof (ShowPointConfig))]
215218
public Color PointColor { get; set; } = Color.Black;
216219

217220
[Caption ("Quality")]
218221
[MinimumValue (1), MaximumValue (4)]
219222
public int Quality { get; set; } = 3;
223+
224+
[Skip]
225+
public bool ShowRandomPointLocation => PointArrangement == PointArrangement.Random;
226+
227+
[Skip]
228+
public bool ShowPointConfig => ShowPoints;
220229
}
221230

222231
public enum ColorSorting

Pinta.Gui.Widgets/Dialogs/ReflectionHelper.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,41 @@ internal static class ReflectionHelper
2020
else
2121
throw new ArgumentException ($"Member \'{name}\' does not exist");
2222
}
23+
24+
/// <summary>
25+
/// Evaluates a condition from a property or method of an object.
26+
/// </summary>
27+
/// <param name="source">The object containing the property or method.</param>
28+
/// <param name="methodName">The name of the property or method to evaluate.</param>
29+
/// <returns>The boolean result of the property or method.</returns>
30+
public static bool EvaluateCondition (object source, string methodName)
31+
=> CreateConditionDelegate (source, methodName) ();
32+
33+
/// <summary>
34+
/// Creates a delegate for evaluating a condition from a property or method of an object.
35+
/// This avoids needing to redo reflection lookups when the condition needs to be evaluated frequently.
36+
/// </summary>
37+
/// <param name="source">The object containing the property or method.</param>
38+
/// <param name="methodName">The name of the property or method to evaluate.</param>
39+
/// <returns>A delegate that evaluates the condition, giving the boolean result of the property or method.</returns>
40+
public static Func<bool> CreateConditionDelegate (object source, string methodName)
41+
{
42+
Type type = source.GetType ();
43+
44+
// Try to find a property first
45+
PropertyInfo? property = type.GetProperty (methodName, BindingFlags.Public | BindingFlags.Instance);
46+
if (property is not null && property.PropertyType == typeof (bool)) {
47+
MethodInfo? getter = property.GetGetMethod ();
48+
if (getter is not null) {
49+
return (Func<bool>) getter.CreateDelegate (typeof (Func<bool>), source);
50+
}
51+
}
52+
// If we couldn't find a property, try to find a method
53+
MethodInfo? method = type.GetMethod (methodName, BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
54+
if (method is not null && method.ReturnType == typeof (bool)) {
55+
return (Func<bool>) method.CreateDelegate (typeof (Func<bool>), source);
56+
}
57+
58+
throw new ArgumentException ($"Member \'{methodName}\' is not a boolean property or method");
59+
}
2360
}

Pinta.Gui.Widgets/Dialogs/SimpleEffectDialog.cs

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ public sealed class SimpleEffectDialog : Gtk.Dialog
4848
private delegate bool TimeoutHandler ();
4949
TimeoutHandler? timeout_func;
5050

51+
private readonly EffectData effect_data;
52+
53+
// Track widgets with conditional visibility/enabled, so we can update
54+
// them when the effect data changes
55+
private sealed record ConditionalWidget (
56+
Gtk.Widget Widget,
57+
Func<bool>? VisibleWhenDelegate,
58+
Func<bool>? EnabledWhenDelegate
59+
);
60+
private readonly List<ConditionalWidget> conditional_widgets = new ();
61+
5162
/// Since this dialog is used by add-ins, the IAddinLocalizer allows for translations to be
5263
/// fetched from the appropriate place.
5364
/// </param>
@@ -82,6 +93,9 @@ public SimpleEffectDialog (
8293
contentAreaBox.Append (widget);
8394

8495
OnClose += (_, _) => HandleClose ();
96+
97+
// Keep reference to effect data, so it can be used for handling conditional widgets
98+
effect_data = effectData;
8599
}
86100

87101
/// <summary>
@@ -132,8 +146,8 @@ private void HandleClose ()
132146
.GetMembers ()
133147
.Where (IsInstanceFieldOrProperty)
134148
.Where (IsCustomProperty)
149+
.Where (member => !member.GetCustomAttributes<SkipAttribute> (false).Any ())
135150
.Select (CreateSettings)
136-
.Where (settings => !settings.skip)
137151
.Select (settings => GenerateWidgetsForMember (settings, effectData, localizer, workspace))
138152
.SelectMany (widgets => widgets);
139153

@@ -160,7 +174,9 @@ private sealed record MemberSettings (
160174
MemberReflector reflector,
161175
string caption,
162176
string? hint,
163-
bool skip);
177+
string? visibleWhenMethodName,
178+
string? enabledWhenMethodName
179+
);
164180

165181
private static MemberSettings CreateSettings (MemberInfo memberInfo)
166182
{
@@ -172,11 +188,24 @@ private static MemberSettings CreateSettings (MemberInfo memberInfo)
172188
.Select (h => h.Caption)
173189
.FirstOrDefault ();
174190

191+
string? visibleConditionMethodName =
192+
reflector.Attributes
193+
.OfType<VisibleWhenAttribute> ()
194+
.Select (v => v.ConditionMethodName)
195+
.FirstOrDefault ();
196+
197+
string? enabledConditionMethodName =
198+
reflector.Attributes
199+
.OfType<EnabledWhenAttribute> ()
200+
.Select (e => e.ConditionMethodName)
201+
.FirstOrDefault ();
202+
175203
return new (
176204
reflector: reflector,
177205
caption: caption ?? MakeCaption (memberInfo.Name),
178206
hint: reflector.Attributes.OfType<HintAttribute> ().Select (h => h.Hint).FirstOrDefault (),
179-
skip: reflector.Attributes.OfType<SkipAttribute> ().Any ());
207+
visibleWhenMethodName: visibleConditionMethodName,
208+
enabledWhenMethodName: enabledConditionMethodName);
180209
}
181210

182211
private static string MakeCaption (string name)
@@ -209,6 +238,19 @@ static IEnumerable<char> GenerateCharacters (string name)
209238
}
210239
}
211240

241+
/// <summary>
242+
/// Updates all widgets with conditional attributes.
243+
/// </summary>
244+
private void UpdateConditionalWidgets (EffectData effectData)
245+
{
246+
foreach (var widget in conditional_widgets) {
247+
if (widget.VisibleWhenDelegate is not null)
248+
widget.Widget.Visible = widget.VisibleWhenDelegate ();
249+
250+
if (widget.EnabledWhenDelegate is not null)
251+
widget.Widget.Sensitive = widget.EnabledWhenDelegate ();
252+
}
253+
}
212254
private IEnumerable<Gtk.Widget> GenerateWidgetsForMember (
213255
MemberSettings settings,
214256
EffectData effectData,
@@ -217,8 +259,36 @@ static IEnumerable<char> GenerateCharacters (string name)
217259
{
218260
WidgetFactory? widgetFactory = GetWidgetFactory (settings);
219261

220-
if (widgetFactory is not null)
221-
yield return widgetFactory (localizer.GetString (settings.caption), effectData, settings, workspace);
262+
if (widgetFactory is not null) {
263+
Gtk.Widget widget = widgetFactory (localizer.GetString (settings.caption), effectData, settings, workspace);
264+
265+
// Keep a reference to widget if it has conditional attributes so we can update it later
266+
if (settings.visibleWhenMethodName is not null || settings.enabledWhenMethodName is not null) {
267+
268+
// Create delegates for condition evaluation (do reflection once, not on every update)
269+
Func<bool>? visibleDelegate = settings.visibleWhenMethodName is not null
270+
? ReflectionHelper.CreateConditionDelegate (effectData, settings.visibleWhenMethodName)
271+
: null;
272+
273+
Func<bool>? enabledDelegate = settings.enabledWhenMethodName is not null
274+
? ReflectionHelper.CreateConditionDelegate (effectData, settings.enabledWhenMethodName)
275+
: null;
276+
277+
// Apply the initial state
278+
if (visibleDelegate is not null)
279+
widget.Visible = visibleDelegate ();
280+
281+
if (enabledDelegate is not null)
282+
widget.Sensitive = enabledDelegate ();
283+
284+
conditional_widgets.Add (new ConditionalWidget (
285+
widget,
286+
visibleDelegate,
287+
enabledDelegate)
288+
);
289+
}
290+
yield return widget;
291+
}
222292

223293
if (settings.hint != null)
224294
yield return CreateHintLabel (localizer.GetString (settings.hint));
@@ -530,6 +600,9 @@ private void SetAndNotify (MemberReflector reflector, object o, object val)
530600
{
531601
reflector.SetValue (o, val);
532602
EffectDataChanged?.Invoke (this, new PropertyChangedEventArgs (reflector.OriginalMemberInfo.Name));
603+
604+
// Update conditional widgets when any property changes
605+
UpdateConditionalWidgets (effect_data);
533606
}
534607

535608
private Gtk.Widget CreateSeed (

0 commit comments

Comments
 (0)