1 /**
2  * Menu module.
3  *
4  * License:
5  *     MIT. See LICENSE for full details.
6  */
7 module tkd.widget.menu.menu;
8 
9 /**
10  * Imports.
11  */
12 import std.array;
13 import std.conv;
14 import std..string;
15 import std.typecons;
16 import tkd.element.element;
17 import tkd.element.uielement;
18 import tkd.image.image;
19 import tkd.image.imageposition;
20 import tkd.widget.menu.menubar;
21 
22 /**
23  * The cascading menu that items are selected from.
24  *
25  * Example:
26  * ---
27  * auto menu = new Menu()
28  * 	.addEntry("Entry 1", delegate(CommandArgs args){ ... })
29  * 	.addEntry("Entry 2", delegate(CommandArgs args){ ... })
30  * 	.addSeparator()
31  * 	.addEntry("Entry 3", delegate(CommandArgs args){ ... });
32  * ---
33  *
34  * Additional_Events:
35  *     Additional events that can also be bound to using the $(LINK2 ../../element/uielement.html#UiElement.bind, bind) method.
36  *     $(P
37  *         <<MenuSelect>>,
38  *         <<PrevWindow>>,
39  *         <Alt-Key>,
40  *         <Button>,
41  *         <ButtonRelease>,
42  *         <Enter>
43  *         <Key-Down>,
44  *         <Key-Escape>,
45  *         <Key-F10>,
46  *         <Key-Left>,
47  *         <Key-Return>,
48  *         <Key-Right>,
49  *         <Key-Tab>,
50  *         <Key-Up>,
51  *         <Key-space>,
52  *         <Key>,
53  *         <Leave>,
54  *         <Motion>,
55  *     )
56  *
57  * See_Also:
58  *     $(LINK2 ../../element/uielement.html, tkd.element.uielement)
59  */
60 class Menu : UiElement
61 {
62 	/**
63 	 * Array containing variables used by check button entries in the menu.
64 	 */
65 	private string[] _checkButtonVariables;
66 
67 	/**
68 	 * The variable used by any radio button entries in the menu.
69 	 */
70 	private string _radioButtonVariable;
71 
72 	/**
73 	 * Construct the widget.
74 	 */
75 	public this()
76 	{
77 		super();
78 		this._elementId = "menu";
79 		this._radioButtonVariable = format("variable-%s", this.generateHash());
80 
81 		this._tk.eval("menu %s -type normal -tearoff 0", this.id);
82 	}
83 
84 	/**
85 	 * Construct the widget.
86 	 *
87 	 * Params:
88 	 *     parent = The parent of this widget.
89 	 *     label = The label of the menu.
90 	 *     underlineChar = The index of the character to underline.
91 	 *
92 	 * See_Also:
93 	 *     $(LINK2 ./menubar.html, tkd.widget.menu.menubar)
94 	 */
95 	public this(MenuBar parent, string label, ubyte underlineChar = ubyte.max)
96 	{
97 		super(parent);
98 		this._elementId = "menu";
99 		this._radioButtonVariable = format("variable-%s", this.generateHash());
100 
101 		this._tk.eval("menu %s -type normal -tearoff 0", this.id);
102 
103 		// String concatenation is used to build the script here instead of 
104 		// using format specifiers to enable supporting input which includes 
105 		// Tcl/Tk reserved characters and elements that could be construed as 
106 		// format specifiers.
107 		string script = std.conv.text(parent.id, ` add cascade -menu `, this.id, ` -label "`, this._tk.escape(label), `" -underline `, underlineChar);
108 		this._tk.eval(script);
109 	}
110 
111 	/**
112 	 * Add a cascade menu to this menu.
113 	 *
114 	 * Params:
115 	 *     label = The label of the menu.
116 	 *     menu = The menu to add as a cascade menu.
117 	 *     underlineChar = The index of the character to underline.
118 	 *
119 	 * Returns:
120 	 *     This widget to aid method chaining.
121 	 */
122 	public auto addMenuEntry(this T)(string label, Menu menu, ubyte underlineChar = ubyte.max)
123 	{
124 		string originalId = menu.id;
125 		menu._parent = this;
126 
127 		this._tk.eval("%s clone %s", originalId, menu.id);
128 
129 		// String concatenation is used to build the script here instead of 
130 		// using format specifiers to enable supporting input which includes 
131 		// Tcl/Tk reserved characters and elements that could be construed as 
132 		// format specifiers.
133 		string script = std.conv.text(this.id, ` add cascade -menu `, menu.id, ` -label "`, this._tk.escape(label), `" -underline `, underlineChar);
134 		this._tk.eval(script);
135 
136 		return cast(T) this;
137 	}
138 
139 	/**
140 	 * Add an item to the menu.
141 	 *
142 	 * Params:
143 	 *     label = The label of the item.
144 	 *     callback = The callback to execute when this item is selected in the menu.
145 	 *     shortCutText = The keyboard shortcut text. This is for decoration only, you must also bind this keypress to an event.
146 	 *
147 	 * Returns:
148 	 *     This widget to aid method chaining.
149 	 *
150 	 * Callback_Arguments:
151 	 *     These are the fields within the callback's $(LINK2 
152 	 *     ../../element/element.html#CommandArgs, CommandArgs) parameter which 
153 	 *     are populated by this method when the callback is executed. 
154 	 *     $(P
155 	 *         $(PARAM_TABLE
156 	 *             $(PARAM_ROW CommandArgs.element, The menu that executed the callback.)
157 	 *             $(PARAM_ROW CommandArgs.uniqueData, The label of the menu entry which was selected.)
158 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
159 	 *         )
160 	 *     )
161 	 *
162 	 * See_Also:
163 	 *     $(LINK2 ../../element/element.html#CommandCallback, tkd.element.element.CommandCallback)
164 	 */
165 	public auto addEntry(this T)(string label, CommandCallback callback, string shortCutText = null)
166 	{
167 		string command = this.createCommand(callback, label);
168 
169 		// String concatenation is used to build the script here instead of 
170 		// using format specifiers to enable supporting input which includes 
171 		// Tcl/Tk reserved characters and elements that could be construed as 
172 		// format specifiers.
173 		string script = std.conv.text(this.id, ` add command -label "`, this._tk.escape(label), `" -command `, command, ` -accelerator "`, this._tk.escape(shortCutText), `"`);
174 		this._tk.eval(script);
175 
176 		return cast(T) this;
177 	}
178 
179 	/**
180 	 * Add an item to the menu with an image.
181 	 *
182 	 * Params:
183 	 *     image = The image of the entry.
184 	 *     label = The label of the item.
185 	 *     callback = The callback to execute when this item is selected in the menu.
186 	 *     shortCutText = The keyboard shortcut text. This is for decoration only, you must also bind this keypress to an event.
187 	 *     imagePosition = The position of the image in relation to the text.
188 	 *
189 	 * Returns:
190 	 *     This widget to aid method chaining.
191 	 *
192 	 * Callback_Arguments:
193 	 *     These are the fields within the callback's $(LINK2 
194 	 *     ../../element/element.html#CommandArgs, CommandArgs) parameter which 
195 	 *     are populated by this method when the callback is executed. 
196 	 *     $(P
197 	 *         $(PARAM_TABLE
198 	 *             $(PARAM_ROW CommandArgs.element, The menu that executed the callback.)
199 	 *             $(PARAM_ROW CommandArgs.uniqueData, The label of the menu entry which was selected.)
200 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
201 	 *         )
202 	 *     )
203 	 *
204 	 * See_Also:
205 	 *     $(LINK2 ../../element/element.html#CommandCallback, tkd.element.element.CommandCallback)
206 	 *     $(LINK2 ../../image/image.html, tkd.image.image) $(BR)
207 	 *     $(LINK2 ../../image/png.html, tkd.image.png) $(BR)
208 	 *     $(LINK2 ../../image/gif.html, tkd.image.gif) $(BR)
209 	 *     $(LINK2 ../../image/imageposition.html, tkd.image.imageposition) $(BR)
210 	 */
211 	public auto addEntry(this T)(Image image, string label, CommandCallback callback, string imagePosition = ImagePosition.left, string shortCutText = null)
212 	{
213 		string command = this.createCommand(callback, label);
214 
215 		// String concatenation is used to build the script here instead of 
216 		// using format specifiers to enable supporting input which includes 
217 		// Tcl/Tk reserved characters and elements that could be construed as 
218 		// format specifiers.
219 		string script = std.conv.text(this.id, ` add command -image `, image.id, ` -label "`, this._tk.escape(label), `" -command `, command, ` -compound `, imagePosition, ` -accelerator "`, this._tk.escape(shortCutText), `"`);
220 		this._tk.eval(script);
221 
222 		return cast(T) this;
223 	}
224 
225 	/**
226 	 * Add an item to the menu that when selected adds a checked icon.
227 	 *
228 	 * Params:
229 	 *     label = The label of the item.
230 	 *     callback = The callback to execute when this item is selected in the menu.
231 	 *     shortCutText = The keyboard shortcut text. This is for decoration only, you must also bind this keypress to an event.
232 	 *
233 	 * Returns:
234 	 *     This widget to aid method chaining.
235 	 *
236 	 * Callback_Arguments:
237 	 *     These are the fields within the callback's $(LINK2 
238 	 *     ../../element/element.html#CommandArgs, CommandArgs) parameter which 
239 	 *     are populated by this method when the callback is executed. 
240 	 *     $(P
241 	 *         $(PARAM_TABLE
242 	 *             $(PARAM_ROW CommandArgs.element, The menu that executed the callback.)
243 	 *             $(PARAM_ROW CommandArgs.uniqueData, The label of the menu entry which was selected.)
244 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
245 	 *         )
246 	 *     )
247 	 *
248 	 * See_Also:
249 	 *     $(LINK2 ../../element/element.html#CommandCallback, tkd.element.element.CommandCallback)
250 	 */
251 	public auto addCheckButtonEntry(this T)(string label, CommandCallback callback, string shortCutText = null)
252 	{
253 		this._checkButtonVariables ~= format("variable-%s", this.generateHash(label));
254 		string command = this.createCommand(callback, label);
255 
256 		// String concatenation is used to build the script here instead of 
257 		// using format specifiers to enable supporting input which includes 
258 		// Tcl/Tk reserved characters and elements that could be construed as 
259 		// format specifiers.
260 		string script = std.conv.text(this.id, ` add checkbutton -label "`, this._tk.escape(label), `" -command `, command, ` -accelerator "`, this._tk.escape(shortCutText), `" -variable `, this._checkButtonVariables.back());
261 		this._tk.eval(script);
262 
263 		return cast(T) this;
264 	}
265 
266 	/**
267 	 * Add an item to the menu with an image that when selected adds a checked icon.
268 	 *
269 	 * Params:
270 	 *     image = The image of the entry.
271 	 *     label = The label of the item.
272 	 *     callback = The callback to execute when this item is selected in the menu.
273 	 *     shortCutText = The keyboard shortcut text. This is for decoration only, you must also bind this keypress to an event.
274 	 *     imagePosition = The position of the image in relation to the text.
275 	 *
276 	 * Returns:
277 	 *     This widget to aid method chaining.
278 	 *
279 	 * Callback_Arguments:
280 	 *     These are the fields within the callback's $(LINK2 
281 	 *     ../../element/element.html#CommandArgs, CommandArgs) parameter which 
282 	 *     are populated by this method when the callback is executed. 
283 	 *     $(P
284 	 *         $(PARAM_TABLE
285 	 *             $(PARAM_ROW CommandArgs.element, The menu that executed the callback.)
286 	 *             $(PARAM_ROW CommandArgs.uniqueData, The label of the menu entry which was selected.)
287 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
288 	 *         )
289 	 *     )
290 	 *
291 	 * See_Also:
292 	 *     $(LINK2 ../../element/element.html#CommandCallback, tkd.element.element.CommandCallback)
293 	 *     $(LINK2 ../../image/image.html, tkd.image.image) $(BR)
294 	 *     $(LINK2 ../../image/png.html, tkd.image.png) $(BR)
295 	 *     $(LINK2 ../../image/gif.html, tkd.image.gif) $(BR)
296 	 *     $(LINK2 ../../image/imageposition.html, tkd.image.imageposition) $(BR)
297 	 */
298 	public auto addCheckButtonEntry(this T)(Image image, string label, CommandCallback callback, string imagePosition = ImagePosition.left, string shortCutText = null)
299 	{
300 		this._checkButtonVariables ~= format("variable-%s", this.generateHash(label));
301 		string command = this.createCommand(callback, label);
302 
303 		// String concatenation is used to build the script here instead of 
304 		// using format specifiers to enable supporting input which includes 
305 		// Tcl/Tk reserved characters and elements that could be construed as 
306 		// format specifiers.
307 		string script = std.conv.text(this.id, ` add checkbutton -image `, image.id, ` -label "`, this._tk.escape(label), `" -command `, command, ` -compound `, imagePosition, ` -accelerator "`, this._tk.escape(shortCutText), `" -variable `, this._checkButtonVariables.back());
308 		this._tk.eval(script);
309 
310 		return cast(T) this;
311 	}
312 
313 	/**
314 	 * Get if the check box entry at the passed index is checked or not. The 
315 	 * index only applies to check box entries in the menu not any other type 
316 	 * of entry. If there are no check box entries in the menu this method 
317 	 * returns false.
318 	 *
319 	 * Params:
320 	 *     index = The index of the check box entry.
321 	 *
322 	 * Returns:
323 	 *     True if the check box entry is selected, false if not.
324 	 */
325 	public bool isCheckBoxEntrySelected(int index)
326 	{
327 		if (index < this._checkButtonVariables.length)
328 		{
329 			return this._tk.getVariable(this._checkButtonVariables[index]).to!(int) == 1;
330 		}
331 		return false;
332 	}
333 
334 	/**
335 	 * Add an item to the menu that acts as a radio button.
336 	 *
337 	 * There can only be one group of radio button entries in one menu. If more 
338 	 * than one group is needed use cascading menus to hold each group.
339 	 *
340 	 * Params:
341 	 *     label = The label of the item.
342 	 *     callback = The callback to execute when this item is selected in the menu.
343 	 *     shortCutText = The keyboard shortcut text. This is for decoration only, you must also bind this keypress to an event.
344 	 *
345 	 * Returns:
346 	 *     This widget to aid method chaining.
347 	 *
348 	 * Callback_Arguments:
349 	 *     These are the fields within the callback's $(LINK2 
350 	 *     ../../element/element.html#CommandArgs, CommandArgs) parameter which 
351 	 *     are populated by this method when the callback is executed. 
352 	 *     $(P
353 	 *         $(PARAM_TABLE
354 	 *             $(PARAM_ROW CommandArgs.element, The menu that executed the callback.)
355 	 *             $(PARAM_ROW CommandArgs.uniqueData, The label of the menu entry which was selected.)
356 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
357 	 *         )
358 	 *     )
359 	 *
360 	 * See_Also:
361 	 *     $(LINK2 ../../element/element.html#CommandCallback, tkd.element.element.CommandCallback)
362 	 */
363 	public auto addRadioButtonEntry(this T)(string label, CommandCallback callback, string shortCutText = null)
364 	{
365 		string command = this.createCommand(callback, label);
366 
367 		// String concatenation is used to build the script here instead of 
368 		// using format specifiers to enable supporting input which includes 
369 		// Tcl/Tk reserved characters and elements that could be construed as 
370 		// format specifiers.
371 		string script = std.conv.text(this.id, ` add radiobutton -label "`, this._tk.escape(label), `" -command `, command, ` -accelerator "`, this._tk.escape(shortCutText), `" -variable `, this._radioButtonVariable);
372 		this._tk.eval(script);
373 
374 		return cast(T) this;
375 	}
376 
377 	/**
378 	 * Add an item to the menu with an image that acts as a radio button.
379 	 *
380 	 * There can only be one group of radio button entries in one menu. If more 
381 	 * than one group is needed use cascading menus to hold each group.
382 	 *
383 	 * Params:
384 	 *     image = The image of the entry.
385 	 *     label = The label of the item.
386 	 *     callback = The callback to execute when this item is selected in the menu.
387 	 *     shortCutText = The keyboard shortcut text. This is for decoration only, you must also bind this keypress to an event.
388 	 *     imagePosition = The position of the image in relation to the text.
389 	 *
390 	 * Returns:
391 	 *     This widget to aid method chaining.
392 	 *
393 	 * Callback_Arguments:
394 	 *     These are the fields within the callback's $(LINK2 
395 	 *     ../../element/element.html#CommandArgs, CommandArgs) parameter which 
396 	 *     are populated by this method when the callback is executed. 
397 	 *     $(P
398 	 *         $(PARAM_TABLE
399 	 *             $(PARAM_ROW CommandArgs.element, The menu that executed the callback.)
400 	 *             $(PARAM_ROW CommandArgs.uniqueData, The label of the menu entry which was selected.)
401 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
402 	 *         )
403 	 *     )
404 	 *
405 	 * See_Also:
406 	 *     $(LINK2 ../../element/element.html#CommandCallback, tkd.element.element.CommandCallback)
407 	 *     $(LINK2 ../../image/image.html, tkd.image.image) $(BR)
408 	 *     $(LINK2 ../../image/png.html, tkd.image.png) $(BR)
409 	 *     $(LINK2 ../../image/gif.html, tkd.image.gif) $(BR)
410 	 *     $(LINK2 ../../image/imageposition.html, tkd.image.imageposition) $(BR)
411 	 */
412 	public auto addRadioButtonEntry(this T)(Image image, string label, CommandCallback callback, string imagePosition = ImagePosition.left, string shortCutText = null)
413 	{
414 		string command = this.createCommand(callback, label);
415 
416 		// String concatenation is used to build the script here instead of 
417 		// using format specifiers to enable supporting input which includes 
418 		// Tcl/Tk reserved characters and elements that could be construed as 
419 		// format specifiers.
420 		string script = std.conv.text(this.id, ` add radiobutton -image `, image.id, ` -label "`, this._tk.escape(label), `" -command `, command, ` -compound `, imagePosition, ` -accelerator "`, this._tk.escape(shortCutText), `" -variable `, this._radioButtonVariable);
421 		this._tk.eval(script);
422 
423 		return cast(T) this;
424 	}
425 
426 	/**
427 	 * Get the value of the selected radio button entry. This value will be the 
428 	 * same as the entry's label. This method will return an empty string if no 
429 	 * radio button entry exists or none are selected.
430 	 *
431 	 * Returns:
432 	 *     The value of the selected radio button entry.
433 	 */
434 	public string getSelectedRadioEntryValue()
435 	{
436 		return this._tk.getVariable(this._radioButtonVariable).to!(string);
437 	}
438 
439 	/**
440 	 * Add a separator to the menu.
441 	 *
442 	 * Returns:
443 	 *     This widget to aid method chaining.
444 	 */
445 	public auto addSeparator(this T)()
446 	{
447 		this._tk.eval("%s add separator", this.id);
448 
449 		return cast(T) this;
450 	}
451 
452 	/**
453 	 * Disable a menu item. The item indexes start at zero for the top-most 
454 	 * entry and increase as you go down. Index refers to all menu items 
455 	 * including separators.
456 	 *
457 	 * Params:
458 	 *     index = The index of the item to disable.
459 	 *
460 	 * Returns:
461 	 *     This widget to aid method chaining.
462 	 */
463 	public auto disableEntry(this T)(int index)
464 	{
465 		this._tk.eval("%s entryconfigure %s -state disable", this.id, index);
466 		
467 		return cast(T) this;
468 	}
469 
470 	/**
471 	 * Enable a menu item. The item indexes start at zero for the top-most 
472 	 * entry and increase as you go down. Index refers to all menu items 
473 	 * including separators.
474 	 *
475 	 * Params:
476 	 *     index = The index of the item to enable.
477 	 *
478 	 * Returns:
479 	 *     This widget to aid method chaining.
480 	 */
481 	public auto enableEntry(this T)(int index)
482 	{
483 		this._tk.eval("%s entryconfigure %s -state normal", this.id, index);
484 		
485 		return cast(T) this;
486 	}
487 
488 	/**
489 	 * Invoke a menu item by its index. The item indexes start at zero for the 
490 	 * top-most entry and increase as you go down. Index refers to all menu 
491 	 * items including separators.
492 	 *
493 	 * Params:
494 	 *     index = The index of the check box entry.
495 	 *
496 	 * Returns:
497 	 *     This widget to aid method chaining.
498 	 */
499 	public auto invoke(this T)(int index)
500 	{
501 		this._tk.eval("%s invoke %s", this.id, index);
502 
503 		return cast(T) this;
504 	}
505 
506 	/**
507 	 * Show the menu.
508 	 *
509 	 * Params:
510 	 *     xPos = The horizontal position of the menu on the screen.
511 	 *     yPos = The vertical position of the menu on the screen.
512 	 *
513 	 * Returns:
514 	 *     This widget to aid method chaining.
515 	 */
516 	public auto popup(this T)(int xPos, int yPos)
517 	{
518 		this._tk.eval("tk_popup %s %s %s", this.id, xPos, yPos);
519 
520 		return cast(T) this;
521 	}
522 }