1 //
2 // SimpleEffectDialog.cs
3 //
4 // Author:
5 //       Jonathan Pobst <monkey@jpobst.com>
6 //
7 // Copyright (c) 2010 Jonathan Pobst
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
15 //
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
18 //
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 // THE SOFTWARE.
26 //
27 // Inspiration and reflection code is from Miguel de Icaza's MIT Licensed MonoTouch.Dialog:
28 // http://github.com/migueldeicaza/MonoTouch.Dialog
29 
30 using System;
31 using System.Reflection;
32 using System.Text;
33 using System.Collections.Generic;
34 using System.ComponentModel;
35 using System.Collections;
36 using Mono.Addins.Localization;
37 using Mono.Unix;
38 
39 namespace Pinta.Gui.Widgets
40 {
41 	public class SimpleEffectDialog : Gtk.Dialog
42 	{
43 		[ThreadStatic]
44 		Random random = new Random ();
45 
46 		const uint event_delay_millis = 100;
47 		uint event_delay_timeout_id;
48 		GLib.TimeoutHandler timeout_func;
49 
50 		/// Since this dialog is used by add-ins, the IAddinLocalizer allows for translations to be
51 		/// fetched from the appropriate place.
52 		/// </param>
SimpleEffectDialog(string title, Gdk.Pixbuf icon, object effectData, IAddinLocalizer localizer)53 		public SimpleEffectDialog (string title, Gdk.Pixbuf icon, object effectData,
54 		                           IAddinLocalizer localizer)
55 			: base (title, Pinta.Core.PintaCore.Chrome.MainWindow, Gtk.DialogFlags.Modal,
56 				Gtk.Stock.Cancel, Gtk.ResponseType.Cancel, Gtk.Stock.Ok, Gtk.ResponseType.Ok)
57 		{
58 			Icon = icon;
59 			EffectData = effectData;
60 
61 			BorderWidth = 6;
62 			VBox.Spacing = 12;
63 			WidthRequest = 400;
64 			Resizable = false;
65 			DefaultResponse = Gtk.ResponseType.Ok;
66 			AlternativeButtonOrder = new int[] { (int)Gtk.ResponseType.Ok, (int)Gtk.ResponseType.Cancel };
67 
68 			BuildDialog (localizer);
69 		}
70 
71 		public object EffectData { get; private set; }
72 
73 		public event PropertyChangedEventHandler EffectDataChanged;
74 
Destroy()75 		public override void Destroy ()
76         {
77             // If there is a timeout that hasn't been invoked yet, run it before closing the dialog.
78             if (event_delay_timeout_id != 0)
79             {
80                 GLib.Source.Remove (event_delay_timeout_id);
81                 timeout_func.Invoke ();
82             }
83 
84             base.Destroy ();
85         }
86 
87 		#region EffectData Parser
BuildDialog(IAddinLocalizer localizer)88 		private void BuildDialog (IAddinLocalizer localizer)
89 		{
90 			var members = EffectData.GetType ().GetMembers ();
91 
92 			foreach (var mi in members) {
93 				Type mType = GetTypeForMember (mi);
94 
95 				if (mType == null)
96 					continue;
97 
98 				string caption = null;
99 				string hint = null;
100 				bool skip = false;
101 				bool combo = false;
102 
103 				object[] attrs = mi.GetCustomAttributes (false);
104 
105 				foreach (var attr in attrs) {
106 					if (attr is SkipAttribute)
107 						skip = true;
108 					else if (attr is CaptionAttribute)
109 						caption = ((CaptionAttribute)attr).Caption;
110 					else if (attr is HintAttribute)
111 						hint = ((HintAttribute)attr).Hint;
112 					else if (attr is StaticListAttribute)
113 						combo = true;
114 
115 				}
116 
117 				if (skip || string.Compare (mi.Name, "IsDefault", true) == 0)
118 					continue;
119 
120 				if (caption == null)
121 					caption = MakeCaption (mi.Name);
122 
123 				if (mType == typeof (int) && (caption == "Seed"))
124 					AddWidget (CreateSeed (localizer.GetString (caption), EffectData, mi, attrs));
125 				else if (mType == typeof (int))
126 					AddWidget (CreateSlider (localizer.GetString (caption), EffectData, mi, attrs));
127 				else if (mType == typeof (double) && (caption == "Angle" || caption == "Rotation"))
128 					AddWidget (CreateAnglePicker (localizer.GetString (caption), EffectData, mi, attrs));
129 				else if (mType == typeof (double))
130 					AddWidget (CreateDoubleSlider (localizer.GetString (caption), EffectData, mi, attrs));
131 				else if (combo && mType == typeof (string))
132 					AddWidget (CreateComboBox (localizer.GetString (caption), EffectData, mi, attrs));
133 				else if (mType == typeof (bool))
134 					AddWidget (CreateCheckBox (localizer.GetString (caption), EffectData, mi, attrs));
135 				else if (mType == typeof (Gdk.Point))
136 					AddWidget (CreatePointPicker (localizer.GetString (caption), EffectData, mi, attrs));
137 				else if (mType == typeof (Cairo.PointD))
138 					AddWidget (CreateOffsetPicker (localizer.GetString (caption), EffectData, mi, attrs));
139 				else if (mType.IsEnum)
140 					AddWidget (CreateEnumComboBox (localizer.GetString (caption), EffectData, mi, attrs));
141 
142 				if (hint != null)
143 					AddWidget (CreateHintLabel (localizer.GetString (hint)));
144 			}
145 		}
146 
AddWidget(Gtk.Widget widget)147 		private void AddWidget (Gtk.Widget widget)
148 		{
149 			widget.Show ();
150 			this.VBox.Add (widget);
151 		}
152 		#endregion
153 
154 		#region Control Builders
CreateEnumComboBox(string caption, object o, System.Reflection.MemberInfo member, System.Object[] attributes)155 		private ComboBoxWidget CreateEnumComboBox (string caption, object o, System.Reflection.MemberInfo member, System.Object[] attributes)
156 		{
157 			Type myType = GetTypeForMember (member);
158 
159 			string[] member_names = Enum.GetNames (myType);
160 			var labels = new List<string> ();
161 			var label_to_member = new Dictionary<string, string> ();
162 
163 			foreach (var member_name in member_names)
164 			{
165 				var members = myType.GetMember (member_name);
166 
167 				// Look for a Caption attribute that provides a (translated) description.
168 				string label;
169 				var attrs = members [0].GetCustomAttributes (typeof (CaptionAttribute), false);
170 				if (attrs.Length > 0)
171 					label = Catalog.GetString (((CaptionAttribute)attrs [0]).Caption);
172 				else
173 					label = Catalog.GetString (member_name);
174 
175 				label_to_member [label] = member_name;
176 				labels.Add (label);
177 			}
178 
179 			ComboBoxWidget widget = new ComboBoxWidget (labels.ToArray ());
180 
181 			widget.Label = caption;
182 			widget.AddEvents ((int)Gdk.EventMask.ButtonPressMask);
183 			widget.Active = ((IList)member_names).IndexOf (GetValue (member, o).ToString ());
184 
185 			widget.Changed += delegate (object sender, EventArgs e) {
186 				SetValue (member, o, Enum.Parse (myType, label_to_member [widget.ActiveText]));
187 			};
188 
189 			return widget;
190 		}
191 
CreateComboBox(string caption, object o, System.Reflection.MemberInfo member, System.Object [] attributes)192 		private ComboBoxWidget CreateComboBox (string caption, object o, System.Reflection.MemberInfo member, System.Object [] attributes)
193 		{
194 			Dictionary<string, object> dict = null;
195 
196 			foreach (var attr in attributes) {
197 				if (attr is StaticListAttribute)
198 					dict = (Dictionary<string, object>)GetValue (((StaticListAttribute)attr).dictionaryName, o);
199 			}
200 
201 			List<string> entries = new List<string> ();
202 			foreach (string str in dict.Keys)
203 				entries.Add (str);
204 
205 			ComboBoxWidget widget = new ComboBoxWidget (entries.ToArray ());
206 
207 			widget.Label = caption;
208 			widget.AddEvents ((int)Gdk.EventMask.ButtonPressMask);
209 			widget.Active = entries.IndexOf ((string)GetValue (member, o));
210 
211 			widget.Changed += delegate (object sender, EventArgs e) {
212 				SetValue (member, o, widget.ActiveText);
213 			};
214 
215 			return widget;
216 		}
217 
CreateDoubleSlider(string caption, object o, MemberInfo member, object [] attributes)218 		private HScaleSpinButtonWidget CreateDoubleSlider (string caption, object o, MemberInfo member, object [] attributes)
219 		{
220 			HScaleSpinButtonWidget widget = new HScaleSpinButtonWidget ();
221 
222 			int min_value = -100;
223 			int max_value = 100;
224 			double inc_value = 0.01;
225 			int digits_value = 2;
226 
227 			foreach (var attr in attributes) {
228 				if (attr is MinimumValueAttribute)
229 					min_value = ((MinimumValueAttribute)attr).Value;
230 				else if (attr is MaximumValueAttribute)
231 					max_value = ((MaximumValueAttribute)attr).Value;
232 				else if (attr is IncrementValueAttribute)
233 					inc_value = ((IncrementValueAttribute)attr).Value;
234 				else if (attr is DigitsValueAttribute)
235 					digits_value = ((DigitsValueAttribute)attr).Value;
236 			}
237 
238 			widget.Label = caption;
239 			widget.MinimumValue = min_value;
240 			widget.MaximumValue = max_value;
241 			widget.IncrementValue = inc_value;
242 			widget.DigitsValue = digits_value;
243 			widget.DefaultValue = (double)GetValue (member, o);
244 
245 			widget.ValueChanged += delegate (object sender, EventArgs e) {
246 				DelayedUpdate( () => {
247 					SetValue (member, o, widget.Value);
248 					return false;
249 				});
250 			};
251 
252 			return widget;
253 		}
254 
CreateSlider(string caption, object o, MemberInfo member, object [] attributes)255 		private HScaleSpinButtonWidget CreateSlider (string caption, object o, MemberInfo member, object [] attributes)
256 		{
257 			HScaleSpinButtonWidget widget = new HScaleSpinButtonWidget ();
258 
259 			int min_value = -100;
260 			int max_value = 100;
261 			double inc_value = 1.0;
262 			int digits_value = 0;
263 
264 			foreach (var attr in attributes) {
265 				if (attr is MinimumValueAttribute)
266 					min_value = ((MinimumValueAttribute)attr).Value;
267 				else if (attr is MaximumValueAttribute)
268 					max_value = ((MaximumValueAttribute)attr).Value;
269 				else if (attr is IncrementValueAttribute)
270 					inc_value = ((IncrementValueAttribute)attr).Value;
271 				else if (attr is DigitsValueAttribute)
272 					digits_value = ((DigitsValueAttribute)attr).Value;
273 			}
274 
275 			widget.Label = caption;
276 			widget.MinimumValue = min_value;
277 			widget.MaximumValue = max_value;
278 			widget.IncrementValue = inc_value;
279 			widget.DigitsValue = digits_value;
280 			widget.DefaultValue = (int)GetValue (member, o);
281 
282 			widget.ValueChanged += delegate (object sender, EventArgs e) {
283 				DelayedUpdate( () => {
284 					SetValue (member, o, widget.ValueAsInt);
285 					return false;
286 				});
287 			};
288 
289 			return widget;
290 		}
291 
CreateCheckBox(string caption, object o, MemberInfo member, object [] attributes)292 		private Gtk.CheckButton CreateCheckBox (string caption, object o, MemberInfo member, object [] attributes)
293 		{
294 			Gtk.CheckButton widget = new Gtk.CheckButton ();
295 
296 			widget.Label = caption;
297 			widget.Active = (bool)GetValue (member, o);
298 
299 			widget.Toggled += delegate (object sender, EventArgs e) {
300 				SetValue (member, o, widget.Active);
301 			};
302 
303 			return widget;
304 		}
305 
CreateOffsetPicker(string caption, object o, MemberInfo member, object [] attributes)306 		private PointPickerWidget CreateOffsetPicker (string caption, object o, MemberInfo member, object [] attributes)
307 		{
308 			PointPickerWidget widget = new PointPickerWidget ();
309 
310 			widget.Label = caption;
311 			widget.DefaultOffset = (Cairo.PointD)GetValue (member, o);
312 
313 			widget.PointPicked += delegate (object sender, EventArgs e) {
314 				SetValue (member, o, widget.Offset);
315 			};
316 
317 			return widget;
318 		}
319 
CreatePointPicker(string caption, object o, MemberInfo member, object [] attributes)320 		private PointPickerWidget CreatePointPicker (string caption, object o, MemberInfo member, object [] attributes)
321 		{
322 			PointPickerWidget widget = new PointPickerWidget ();
323 
324 			widget.Label = caption;
325 			widget.DefaultPoint = (Gdk.Point)GetValue (member, o);
326 
327 			widget.PointPicked += delegate (object sender, EventArgs e) {
328 				SetValue (member, o, widget.Point);
329 			};
330 
331 			return widget;
332 		}
333 
CreateAnglePicker(string caption, object o, MemberInfo member, object [] attributes)334 		private AnglePickerWidget CreateAnglePicker (string caption, object o, MemberInfo member, object [] attributes)
335 		{
336 			AnglePickerWidget widget = new AnglePickerWidget ();
337 
338 			widget.Label = caption;
339 			widget.DefaultValue = (double)GetValue (member, o);
340 
341 			widget.ValueChanged += delegate (object sender, EventArgs e) {
342 				DelayedUpdate( () => {
343 					SetValue (member, o, widget.Value);
344 					return false;
345 				});
346 			};
347 
348 			return widget;
349 		}
350 
CreateHintLabel(string hint)351 		private Gtk.Label CreateHintLabel (string hint)
352 		{
353 			Gtk.Label label = new Gtk.Label (hint);
354 			label.LineWrap = true;
355 
356 			return label;
357 		}
358 
CreateSeed(string caption, object o, MemberInfo member, object [] attributes)359 		private ReseedButtonWidget CreateSeed (string caption, object o, MemberInfo member, object [] attributes)
360 		{
361 			ReseedButtonWidget widget = new ReseedButtonWidget ();
362 
363 			widget.Clicked += delegate (object sender, EventArgs e) {
364 				SetValue (member, o, random.Next ());
365 			};
366 
367 			return widget;
368 		}
369 		#endregion
370 
371 		#region Static Reflection Methods
GetValue(MemberInfo mi, object o)372 		private static object GetValue (MemberInfo mi, object o)
373 		{
374 			var fi = mi as FieldInfo;
375 			if (fi != null)
376 				return fi.GetValue (o);
377 			var pi = mi as PropertyInfo;
378 
379 			var getMethod = pi.GetGetMethod ();
380 			return getMethod.Invoke (o, new object [0]);
381 		}
382 
SetValue(MemberInfo mi, object o, object val)383 		private void SetValue (MemberInfo mi, object o, object val)
384 		{
385 			var fi = mi as FieldInfo;
386 			var pi = mi as PropertyInfo;
387 			string fieldName = null;
388 
389 			if (fi != null) {
390 				fi.SetValue (o, val);
391 				fieldName = fi.Name;
392 			} else if (pi != null) {
393 				var setMethod = pi.GetSetMethod ();
394 				setMethod.Invoke (o, new object [] { val });
395 				fieldName = pi.Name;
396 			}
397 
398 			if (EffectDataChanged != null)
399 				EffectDataChanged (this, new PropertyChangedEventArgs (fieldName));
400 		}
401 
402 		// Returns the type for fields and properties and null for everything else
GetTypeForMember(MemberInfo mi)403 		private static Type GetTypeForMember (MemberInfo mi)
404 		{
405 			if (mi is FieldInfo)
406 				return ((FieldInfo)mi).FieldType;
407 			else if (mi is PropertyInfo)
408 				return ((PropertyInfo)mi).PropertyType;
409 
410 			return null;
411 		}
412 
MakeCaption(string name)413 		private static string MakeCaption (string name)
414 		{
415 			var sb = new StringBuilder (name.Length);
416 			bool nextUp = true;
417 
418 			foreach (char c in name) {
419 				if (nextUp) {
420 					sb.Append (Char.ToUpper (c));
421 					nextUp = false;
422 				} else {
423 					if (c == '_') {
424 						sb.Append (' ');
425 						nextUp = true;
426 						continue;
427 					}
428 					if (Char.IsUpper (c))
429 						sb.Append (' ');
430 					sb.Append (c);
431 				}
432 			}
433 
434 			return sb.ToString ();
435 		}
436 
GetValue(string name, object o)437 		private object GetValue (string name, object o)
438 		{
439 			var fi = o.GetType ().GetField (name);
440 			if (fi != null)
441 				return fi.GetValue (o);
442 			var pi = o.GetType ().GetProperty (name);
443 			if (pi == null)
444 				return null;
445 			var getMethod = pi.GetGetMethod ();
446 			return getMethod.Invoke (o, new object [0]);
447 		}
448 
DelayedUpdate(GLib.TimeoutHandler handler)449         private void DelayedUpdate (GLib.TimeoutHandler handler)
450         {
451             if (event_delay_timeout_id != 0)
452 			{
453 				GLib.Source.Remove (event_delay_timeout_id);
454 				if (handler != timeout_func)
455 					timeout_func.Invoke ();
456 			}
457 
458 			timeout_func = handler;
459 			event_delay_timeout_id = GLib.Timeout.Add (event_delay_millis, () => {
460 				event_delay_timeout_id = 0;
461 				timeout_func.Invoke ();
462 				timeout_func = null;
463 				return false;
464 			});
465         }
466         #endregion
467     }
468 
469 	/// <summary>
470 	/// Wrapper around Pinta's translation template.
471 	/// </summary>
472 	public class PintaLocalizer : IAddinLocalizer
473 	{
GetString(string msgid)474 		public string GetString (string msgid)
475 		{
476 			return Mono.Unix.Catalog.GetString (msgid);
477 		}
478 	};
479 }
480