1 /**
2  * Widget module.
3  *
4  * License:
5  *     MIT. See LICENSE for full details.
6  */
7 module tkd.widget.widget;
8 
9 /**
10  * Imports.
11  */
12 import std.algorithm;
13 import std.array;
14 import std..string;
15 import tkd.element.element;
16 import tkd.element.uielement;
17 import tkd.widget.anchorposition;
18 import tkd.widget.state;
19 
20 /**
21  * The widget base class.
22  *
23  * See_Also:
24  *     $(LINK2 ../element/uielement.html, tkd.element.uielement)
25  */
26 abstract class Widget : UiElement
27 {
28 	/**
29 	 * Construct the widget.
30 	 *
31 	 * Params:
32 	 *     parent = An optional parent of this widget.
33 	 *
34 	 * See_Also:
35 	 *     $(LINK2 ../element/uielement.html, tkd.element.uielement)
36 	 */
37 	public this(UiElement parent = null)
38 	{
39 		super(parent);
40 
41 		this._elementId = "widget";
42 	}
43 
44 	/**
45 	 * Set the widget's state.
46 	 *
47 	 * Params:
48 	 *     state = An array of widget states.
49 	 *
50 	 * Returns:
51 	 *     This widget to aid method chaining.
52 	 *
53 	 * See_Also:
54 	 *     $(LINK2 ./state.html, tkd.widget.state)
55 	 */
56 	public auto setState(this T)(string[] state)
57 	{
58 		this._tk.eval("%s state { %s }", this.id, state.join(" "));
59 
60 		return cast(T) this;
61 	}
62 
63 	/**
64 	 * Get the widget's state.
65 	 *
66 	 * Returns:
67 	 *     An array of widget states.
68 	 *
69 	 * See_Also:
70 	 *     $(LINK2 ./state.html, tkd.widget.state)
71 	 */
72 	public string[] getState()
73 	{
74 		this._tk.eval("%s state", this.id);
75 		return this._tk.getResult!(string).split();
76 	}
77 
78 	/**
79 	 * Test if a widget is in a particular state.
80 	 *
81 	 * Params:
82 	 *     state = An array of widget states.
83 	 *
84 	 * Returns:
85 	 *     true is the widget is in that state, false if not.
86 	 *
87 	 * See_Also:
88 	 *     $(LINK2 ./state.html, tkd.widget.state)
89 	 */
90 	public bool inState(string[] state)
91 	{
92 		if (state.canFind(State.normal))
93 		{
94 			throw new Exception("State.normal is not supported by inState method.");
95 		}
96 
97 		this._tk.eval("%s instate { %s }", this.id, state.join(" "));
98 		return this._tk.getResult!(int) == 1;
99 	}
100 
101 	/**
102 	 * Remove the widget's state.
103 	 *
104 	 * Params:
105 	 *     state = An array of widget states.
106 	 *
107 	 * Returns:
108 	 *     This widget to aid method chaining.
109 	 *
110 	 * See_Also:
111 	 *     $(LINK2 ./state.html, tkd.widget.state)
112 	 */
113 	public auto removeState(this T)(string[] state)
114 	{
115 		this._tk.eval("%s state { !%s }", this.id, state.join(" !"));
116 
117 		return cast(T) this;
118 	}
119 
120 	/**
121 	 * Reset the widget's state to default.
122 	 *
123 	 * Returns:
124 	 *     This widget to aid method chaining.
125 	 *
126 	 * See_Also:
127 	 *     $(LINK2 ./state.html, tkd.widget.state)
128 	 */
129 	public auto resetState(this T)()
130 	{
131 		this.removeState(this.getState());
132 
133 		return cast(T) this;
134 	}
135 
136 	/**
137 	 * Set the widget's style.
138 	 *
139 	 * Params:
140 	 *     style = A widget style.
141 	 *
142 	 * Returns:
143 	 *     This widget to aid method chaining.
144 	 *
145 	 * See_Also:
146 	 *     $(LINK2 ./style.html, tkd.widget.style)
147 	 */
148 	public auto setStyle(this T)(string style)
149 	{
150 		this._tk.eval("%s configure -style %s", this.id, style);
151 
152 		return cast(T) this;
153 	}
154 
155 	/**
156 	 * Get the widget's style.
157 	 *
158 	 * Returns:
159 	 *     The widget's style.
160 	 *
161 	 * See_Also:
162 	 *     $(LINK2 ./style.html, tkd.widget.style)
163 	 */
164 	public string getStyle()
165 	{
166 		this._tk.eval("%s cget -style", this.id);
167 		if (this._tk.getResult!(string).empty())
168 		{
169 			return this.getClass();
170 		}
171 		return this._tk.getResult!(string);
172 	}
173 
174 	/**
175 	 * Set if the widget can recieve focus during keyboard traversal.
176 	 *
177 	 * Params:
178 	 *     focus = A focus setting.
179 	 *
180 	 * Returns:
181 	 *     This widget to aid method chaining.
182 	 *
183 	 * See_Also:
184 	 *     $(LINK2 ./keyboardfocus.html, tkd.widget.keyboardfocus)
185 	 */
186 	public auto setKeyboardFocus(this T)(string focus)
187 	{
188 		this._tk.eval("%s configure -takefocus %s", this.id, focus);
189 
190 		return cast(T) this;
191 	}
192 
193 	/**
194 	 * Get if the widget can recieve focus during keyboard traversal.
195 	 *
196 	 * Returns:
197 	 *     The widget's focus setting.
198 	 *
199 	 * See_Also:
200 	 *     $(LINK2 ./keyboardfocus.html, tkd.widget.keyboardfocus)
201 	 */
202 	public string getKeyboardFocus()
203 	{
204 		this._tk.eval("%s cget -takefocus", this.id);
205 		return this._tk.getResult!(string);
206 	}
207 
208 	/**
209 	 * Geometry method for loosely placing this widget inside its parent using 
210 	 * a web browser model. Widgets flow around each other in the available space.
211 	 *
212 	 * Params:
213 	 *     outerPadding = The amound of padding to add around the widget.
214 	 *     innerPadding = The amound of padding to add inside the widget.
215 	 *     side = The side to place the widget inside its parent.
216 	 *     fill = The space to fill inside its parent.
217 	 *     anchor = The anchor position of the widget inside its parent.
218 	 *     expand = Whether or not to expand to fill the entire given space.
219 	 *
220 	 * Returns:
221 	 *     This widget to aid method chaining.
222 	 *
223 	 * See_Also:
224 	 *     $(LINK2 ../element/uielement.html#UiElement.enableGeometryAutoSize, tkd.element.uielement.UiElement.enableGeometryAutoSize) $(BR)
225 	 *     $(LINK2 ./anchorposition.html, tkd.widget.anchorposition) $(BR)
226 	 *     $(LINK2 ./widget.html#GeometryFill, tkd.widget.widget.GeometryFill) $(BR)
227 	 *     $(LINK2 ./widget.html#GeometrySide, tkd.widget.widget.GeometrySide) $(BR)
228 	 */
229 	public auto pack(this T)(int outerPadding = 0, int innerPadding = 0, string side = GeometrySide.top, string fill = GeometryFill.none, string anchor = AnchorPosition.center, bool expand = false)
230 	{
231 		this._tk.eval("pack %s -padx %s -pady %s -ipadx %s -ipady %s -side {%s} -fill {%s} -anchor {%s} -expand %s", this.id, outerPadding, outerPadding, innerPadding, innerPadding, side, fill, anchor, expand);
232 
233 		return cast(T) this;
234 	}
235 
236 	/**
237 	 * Geometry method for placing this widget inside its parent using an 
238 	 * imaginary grid. Somewhat more direct and intuitive than pack. Choose 
239 	 * grid for tabular layouts, and when there's no good reason to choose 
240 	 * something else.
241 	 *
242 	 * If a widget's cell is larger than its default dimensions, the sticky 
243 	 * parameter may be used to position (or stretch) the widget within its 
244 	 * cell. The sticky argument is a string that contains zero or more of the 
245 	 * characters n, s, e or w. Each letter refers to a side (north, south, 
246 	 * east, or west) that the widget will 'stick' to. If both n and s (or e 
247 	 * and w) are specified, the slave will be stretched to fill the entire 
248 	 * height (or width) of its cell. The sticky parameter subsumes the 
249 	 * combination of anchor and fill that is used by pack. The default is an 
250 	 * empty string, which causes the widget to be centered in its cell, at its 
251 	 * default size. The $(LINK2 ./anchorposition.html, anchorposition) enum 
252 	 * may be of use here but doesn't provide all combinations.
253 	 *
254 	 * Params:
255 	 *     column = The column in which to place this widget.
256 	 *     row = The row in which to place this widget.
257 	 *     outerPadding = The amound of padding to add around the widget.
258 	 *     innerPadding = The amound of padding to add inside the widget.
259 	 *     columnSpan = The amount of column this widget should span across.
260 	 *     rowSpan = The amount of rows this widget should span across.
261 	 *     sticky = Which edges of the cell the widget should touch. See note above.
262 	 *
263 	 * Returns:
264 	 *     This widget to aid method chaining.
265 	 *
266 	 * Caveats:
267 	 *     In order for a gridded UI to be fully dynamic to expand and contract 
268 	 *     when resizing elements, you must to assign a weight to at least one 
269 	 *     column and row using $(LINK2 
270 	 *     ../element/uielement.html#UiElement.configureGeometryColumn, 
271 	 *     configureGeometryColumn) and $(LINK2 
272 	 *     ../element/uielement.html#UiElement.configureGeometryRow, 
273 	 *     configureGeometryRow) respectively.
274 	 *
275 	 * See_Also:
276 	 *     $(LINK2 ../element/uielement.html#UiElement.configureGeometryColumn, tkd.element.uielement.UiElement.configureGeometryColumn) $(BR)
277 	 *     $(LINK2 ../element/uielement.html#UiElement.configureGeometryRow, tkd.element.uielement.UiElement.configureGeometryRow) $(BR)
278 	 *     $(LINK2 ../element/uielement.html#UiElement.enableGeometryAutoSize, tkd.element.uielement.UiElement.enableGeometryAutoSize) $(BR)
279 	 *     $(LINK2 ./anchorposition.html, tkd.widget.anchorposition) $(BR)
280 	 */
281 	public auto grid(this T)(int column, int row, int outerPadding = 0, int innerPadding = 0, int columnSpan = 1, int rowSpan = 1, string sticky = "")
282 	{
283 		this._tk.eval("grid %s -column %s -row %s -padx %s -pady %s -ipadx %s -ipady %s -columnspan %s -rowspan %s -sticky {%s}", this.id, column, row, outerPadding, outerPadding, innerPadding, innerPadding, columnSpan, rowSpan, sticky);
284 
285 		return cast(T) this;
286 	}
287 
288 	/**
289 	 * Geometry method for placing this widget inside its parent using absolute 
290 	 * positioning.
291 	 *
292 	 * Params:
293 	 *     xPos = The horizontal position of the widget inside its parent.
294 	 *     yPos = The vertical position of the widget inside its parent.
295 	 *     width = The width of the widget.
296 	 *     height = The height of the widget.
297 	 *     anchor = The anchor position of the widget inside its parent.
298 	 *     borderMode = How the widget interacts with the parent's border.
299 	 *
300 	 * Returns:
301 	 *     This widget to aid method chaining.
302 	 *
303 	 * See_Also:
304 	 *     $(LINK2 ./anchorposition.html, tkd.widget.anchorposition) $(BR)
305 	 *     $(LINK2 ./widget.html#GeometryBorderMode, tkd.widget.widget.GeometryBorderMode) $(BR)
306 	 */
307 	public auto place(this T)(int xPos, int yPos, int width, int height, string anchor = AnchorPosition.northWest, string borderMode = GeometryBorderMode.inside)
308 	{
309 		this._tk.eval("place %s -x %s -y %s -width %s -height %s -anchor {%s} -bordermode {%s}", this.id, xPos, yPos, width, height, anchor, borderMode);
310 
311 		return cast(T) this;
312 	}
313 
314 	/**
315 	 * Geometry method for placing this widget inside its parent using relative 
316 	 * positioning. In this case the position and size is specified as a 
317 	 * floating-point number between 0.0 and 1.0 relative to the height of the 
318 	 * parent. 0.5 means the widget will be half as high as the parent and 1.0 
319 	 * means the widget will have the same height as the parent, and so on.
320 	 *
321 	 * Params:
322 	 *     relativeXPos = The relative horizontal position of the widget inside its parent.
323 	 *     relativeYPos = The relative vertical position of the widget inside its parent.
324 	 *     relativeWidth = The relative width of the widget.
325 	 *     relativeHeight = The relative height of the widget.
326 	 *     anchor = The anchor position of the widget inside its parent.
327 	 *     borderMode = How the widget interacts with the parent's border.
328 	 *
329 	 * Returns:
330 	 *     This widget to aid method chaining.
331 	 *
332 	 * See_Also:
333 	 *     $(LINK2 ./anchorposition.html, tkd.widget.anchorposition) $(BR)
334 	 *     $(LINK2 ./widget.html#GeometryBorderMode, tkd.widget.widget.GeometryBorderMode) $(BR)
335 	 */
336 	public auto place(this T)(double relativeXPos, double relativeYPos, double relativeWidth, double relativeHeight, string anchor = AnchorPosition.northWest, string borderMode = GeometryBorderMode.inside)
337 	{
338 		assert(relativeXPos >= 0 && relativeXPos <= 1);
339 		assert(relativeYPos >= 0 && relativeYPos <= 1);
340 		assert(relativeWidth >= 0 && relativeWidth <= 1);
341 		assert(relativeHeight >= 0 && relativeHeight <= 1);
342 
343 		this._tk.eval("place %s -relx %s -rely %s -relwidth %s -relheight %s -anchor {%s} -bordermode {%s}", this.id, relativeXPos, relativeYPos, relativeWidth, relativeHeight, anchor, borderMode);
344 
345 		return cast(T) this;
346 	}
347 }
348 
349 /**
350  * Side values for widget geometry layout.
351  */
352 enum GeometrySide : string
353 {
354 	left   = "left",   /// Set geometry to the left.
355 	right  = "right",  /// Set geometry to the right.
356 	top    = "top",    /// Set geometry to the top.
357 	bottom = "bottom", /// Set geometry to the bottom.
358 }
359 
360 /**
361  * Fill values for widget geometry layout.
362  */
363 enum GeometryFill : string
364 {
365 	none = "none", /// No filling.
366 	x    = "x",    /// Fill the available horizontal space.
367 	y    = "y",    /// Fill the available vertical space.
368 	both = "both", /// Fill all available space.
369 }
370 
371 /**
372  * Interaction modes for parent borders.
373  */
374 enum GeometryBorderMode : string
375 {
376 	inside  = "inside",  /// Take into consideration the border when placing widgets.
377 	outside = "outside", /// Don't take consideration the border when placing widgets.
378 	ignore  = "ignore",  /// Ignore borders.
379 }