1 // Written in the D programming language.
2
3 /**
4 This module contains declaration of Widget class - base class for all widgets.
5
6 Widgets are styleable. Use styleId property to set style to use from current Theme.
7
8 When any of styleable attributes is being overriden, widget's own copy of style is being created to hold modified attributes (defaults to parent style).
9
10 Two phase layout model (like in Android UI) is used - measure() call is followed by layout() is used to measure and layout widget and its children.abstract
11
12 Method onDraw will be called to draw widget on some surface. Widget.onDraw() draws widget background (if any).
13
14
15 Synopsis:
16
17 ----
18 import dlangui.widgets.widget;
19
20 // access attributes as properties
21 auto w = new Widget("id1");
22 w.backgroundColor = 0xFFFF00;
23 w.layoutWidth = FILL_PARENT;
24 w.layoutHeight = FILL_PARENT;
25 w.padding(Rect(10,10,10,10));
26 // same, but using chained method call
27 auto w = new Widget("id1").backgroundColor(0xFFFF00).layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).padding(Rect(10,10,10,10));
28
29
30 ----
31
32 Copyright: Vadim Lopatin, 2014
33 License: Boost License 1.0
34 Authors: Vadim Lopatin, coolreader.org@gmail.com
35 */
36 module dlangui.widgets.widget;
37
38 public {
39 import dlangui.core.types;
40 import dlangui.core.events;
41 import dlangui.core.i18n;
42 import dlangui.core.collections;
43 import dlangui.widgets.styles;
44
45 import dlangui.graphics.drawbuf;
46 import dlangui.graphics.resources;
47 import dlangui.graphics.fonts;
48 import dlangui.graphics.colors;
49
50 import dlangui.core.signals;
51
52 import dlangui.platforms.common.platform;
53 import dlangui.dml.annotations;
54 }
55
56 import std.algorithm;
57
58
59 /// Visibility (see Android View Visibility)
60 enum Visibility : ubyte {
61 /// Visible on screen (default)
62 Visible,
63 /// Not visible, but occupies a space in layout
64 Invisible,
65 /// Completely hidden, as not has been added
66 Gone
67 }
68
69 enum Orientation : ubyte {
70 Vertical,
71 Horizontal
72 }
73
74 enum FocusReason : ubyte {
75 TabFocus,
76 Unspecified
77 }
78
79 /// interface - slot for onClick
80 interface OnClickHandler {
81 bool onClick(Widget source);
82 }
83
84 /// interface - slot for onCheckChanged
85 interface OnCheckHandler {
86 bool onCheckChanged(Widget source, bool checked);
87 }
88
89 /// interface - slot for onFocusChanged
90 interface OnFocusHandler {
91 bool onFocusChanged(Widget source, bool focused);
92 }
93
94 /// interface - slot for onKey
95 interface OnKeyHandler {
96 bool onKey(Widget source, KeyEvent event);
97 }
98
99 /// interface - slot for keyToAction
100 interface OnKeyActionHandler {
101 Action findKeyAction(Widget source, uint keyCode, uint keyFlags);
102 }
103
104 /// interface - slot for onAction
105 interface OnActionHandler {
106 bool onAction(Widget source, const Action action);
107 }
108
109 /// interface - slot for onMouse
110 interface OnMouseHandler {
111 bool onMouse(Widget source, MouseEvent event);
112 }
113
114 /// focus movement options
115 enum FocusMovement {
116 /// no focus movement
117 None,
118 /// next focusable (Tab)
119 Next,
120 /// previous focusable (Shift+Tab)
121 Previous,
122 /// move to nearest above
123 Up,
124 /// move to nearest below
125 Down,
126 /// move to nearest at left
127 Left,
128 /// move to nearest at right
129 Right,
130 }
131
132 /// standard mouse cursor types
133 enum CursorType {
134 None,
135 /// use parent's cursor
136 Parent,
137 Arrow,
138 IBeam,
139 Wait,
140 Crosshair,
141 WaitArrow,
142 SizeNWSE,
143 SizeNESW,
144 SizeWE,
145 SizeNS,
146 SizeAll,
147 No,
148 Hand
149 }
150
151 /**
152 * Base class for all widgets.
153 *
154 */
155 @dmlwidget
156 class Widget {
157 protected:
158 /// widget id
159 string _id;
160 /// current widget position, set by layout()
161 Rect _pos;
162 /// widget visibility: either Visible, Invisible, Gone
163 Visibility _visibility = Visibility.Visible; // visible by default
164 /// style id to lookup style in theme
165 string _styleId;
166 /// own copy of style - to override some of style properties, null of no properties overriden
167 Style _ownStyle;
168
169 /// widget state (set of flags from State enum)
170 uint _state;
171
172 /// width measured by measure()
173 int _measuredWidth;
174 /// height measured by measure()
175 int _measuredHeight;
176 /// true to force layout
177 bool _needLayout = true;
178 /// true to force redraw
179 bool _needDraw = true;
180 /// parent widget
181 Widget _parent;
182 /// window (to be used for top level widgets only!)
183 Window _window;
184
185 /// does widget need to track mouse Hover
186 bool _trackHover;
187
188 public:
189 /// mouse movement processing flag (when true, widget will change Hover state while mouse is moving)
190 @property bool trackHover() const { return _trackHover && !TOUCH_MODE; }
191 /// set new trackHover flag value (when true, widget will change Hover state while mouse is moving)
192 @property Widget trackHover(bool v) { _trackHover = v; return this; }
193
194 /// returns mouse cursor type for widget
195 uint getCursorType(int x, int y) {
196 return CursorType.Arrow;
197 }
198
199 /// empty parameter list constructor - for usage by factory
200 this() {
201 this(null);
202 }
203 /// create with ID parameter
204 this(string ID) {
205 _id = ID;
206 _state = State.Enabled;
207 _cachedStyle = currentTheme.get(null);
208 debug _instanceCount++;
209 //Log.d("Created widget, count = ", ++_instanceCount);
210 }
211
212 debug {
213 private static __gshared int _instanceCount = 0;
214 /// for debug purposes - number of created widget objects, not yet destroyed
215 static @property int instanceCount() { return _instanceCount; }
216 }
217
218 ~this() {
219 debug {
220 //Log.v("destroying widget ", _id, " ", this.classinfo.name);
221 if (appShuttingDown)
222 onResourceDestroyWhileShutdown(_id, this.classinfo.name);
223 _instanceCount--;
224 }
225 if (_ownStyle !is null)
226 destroy(_ownStyle);
227 _ownStyle = null;
228 //Log.d("Destroyed widget, count = ", --_instanceCount);
229 }
230
231
232 // Caching a style to decrease a number of currentTheme.get calls.
233 private Style _cachedStyle;
234 /// accessor to style - by lookup in theme by styleId (if style id is not set, theme base style will be used).
235 protected @property const (Style) style() const {
236 if (_ownStyle !is null)
237 return _ownStyle;
238 if(_cachedStyle !is null)
239 return _cachedStyle;
240 return currentTheme.get(_styleId);
241 }
242 /// accessor to style - by lookup in theme by styleId (if style id is not set, theme base style will be used).
243 protected @property const (Style) style(uint stateFlags) const {
244 const (Style) normalStyle = style();
245 if (stateFlags == State.Normal) // state is normal
246 return normalStyle;
247 const (Style) stateStyle = normalStyle.forState(stateFlags);
248 if (stateStyle !is normalStyle)
249 return stateStyle; // found style for state in current style
250 //// lookup state style in parent (one level max)
251 //const (Style) parentStyle = normalStyle.parentStyle;
252 //if (parentStyle is normalStyle)
253 // return normalStyle; // no parent
254 //const (Style) parentStateStyle = parentStyle.forState(stateFlags);
255 //if (parentStateStyle !is parentStyle)
256 // return parentStateStyle; // found style for state in parent
257 return normalStyle; // fallback to current style
258 }
259 /// returns style for current widget state
260 protected @property const(Style) stateStyle() const {
261 return style(state);
262 }
263
264 /// enforces widget's own style - allows override some of style properties
265 @property Style ownStyle() {
266 if (_ownStyle is null)
267 _ownStyle = currentTheme.modifyStyle(_styleId);
268 return _ownStyle;
269 }
270
271 /// handle theme change: e.g. reload some themed resources
272 void onThemeChanged() {
273 // default implementation: call recursive for children
274 for (int i = 0; i < childCount; i++)
275 child(i).onThemeChanged();
276 if (_ownStyle) {
277 _ownStyle.onThemeChanged();
278 }
279 if (_cachedStyle) {
280 _cachedStyle = currentTheme.get(_styleId);
281 }
282 }
283
284 /// returns widget id, null if not set
285 @property string id() const { return _id; }
286 /// set widget id
287 @property Widget id(string id) { _id = id; return this; }
288 /// compare widget id with specified value, returs true if matches
289 bool compareId(string id) const { return (_id !is null) && id.equal(_id); }
290
291 /// widget state (set of flags from State enum)
292 @property uint state() const {
293 if ((_state & State.Parent) != 0 && _parent !is null)
294 return _parent.state;
295 if (focusGroupFocused)
296 return _state | State.WindowFocused; // TODO:
297 return _state;
298 }
299 /// override to handle focus changes
300 protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
301 invalidate();
302 focusChange(this, focused);
303 }
304 /// override to handle check changes
305 protected void handleCheckChange(bool checked) {
306 invalidate();
307 checkChange(this, checked);
308 }
309 /// set new widget state (set of flags from State enum)
310 @property Widget state(uint newState) {
311 if ((_state & State.Parent) != 0 && _parent !is null)
312 return _parent.state(newState);
313 if (newState != _state) {
314 uint oldState = _state;
315 _state = newState;
316 // need to redraw
317 invalidate();
318 // notify focus changes
319 if ((oldState & State.Focused) && !(newState & State.Focused))
320 handleFocusChange(false);
321 else if (!(oldState & State.Focused) && (newState & State.Focused))
322 handleFocusChange(true, cast(bool)(newState & State.KeyboardFocused));
323 // notify checked changes
324 if ((oldState & State.Checked) && !(newState & State.Checked))
325 handleCheckChange(false);
326 else if (!(oldState & State.Checked) && (newState & State.Checked))
327 handleCheckChange(true);
328 }
329 return this;
330 }
331 /// add state flags (set of flags from State enum)
332 @property Widget setState(uint stateFlagsToSet) {
333 return state(state | stateFlagsToSet);
334 }
335 /// remove state flags (set of flags from State enum)
336 @property Widget resetState(uint stateFlagsToUnset) {
337 return state(state & ~stateFlagsToUnset);
338 }
339
340
341
342 //======================================================
343 // Style related properties
344
345 /// returns widget style id, null if not set
346 @property string styleId() const { return _styleId; }
347 /// set widget style id
348 @property Widget styleId(string id) {
349 _styleId = id;
350 if (_ownStyle)
351 _ownStyle.parentStyleId = id;
352 _cachedStyle = currentTheme.get(id);
353 return this;
354 }
355 /// get margins (between widget bounds and its background)
356 @property Rect margins() const { return style.margins; }
357 /// set margins for widget - override one from style
358 @property Widget margins(Rect rc) {
359 ownStyle.margins = rc;
360 requestLayout();
361 return this;
362 }
363 /// set margins for widget with the same value for left, top, right, bottom - override one from style
364 @property Widget margins(int v) {
365 ownStyle.margins = Rect(v, v, v, v);
366 requestLayout();
367 return this;
368 }
369 immutable static int FOCUS_RECT_PADDING = 2;
370 /// get padding (between background bounds and content of widget)
371 @property Rect padding() const {
372 // get max padding from style padding and background drawable padding
373 Rect p = style.padding;
374 DrawableRef d = backgroundDrawable;
375 if (!d.isNull) {
376 Rect dp = d.padding;
377 if (p.left < dp.left)
378 p.left = dp.left;
379 if (p.right < dp.right)
380 p.right = dp.right;
381 if (p.top < dp.top)
382 p.top = dp.top;
383 if (p.bottom < dp.bottom)
384 p.bottom = dp.bottom;
385 }
386 if ((focusable || ((state & State.Parent) && parent.focusable)) && focusRectColors) {
387 // add two pixels to padding when focus rect is required - one pixel for focus rect, one for additional space
388 p.offset(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
389 }
390 return p;
391 }
392 /// set padding for widget - override one from style
393 @property Widget padding(Rect rc) {
394 ownStyle.padding = rc;
395 requestLayout();
396 return this;
397 }
398 /// set padding for widget to the same value for left, top, right, bottom - override one from style
399 @property Widget padding(int v) {
400 ownStyle.padding = Rect(v, v, v, v);
401 requestLayout();
402 return this;
403 }
404 /// returns background color
405 @property uint backgroundColor() const { return stateStyle.backgroundColor; }
406 /// set background color for widget - override one from style
407 @property Widget backgroundColor(uint color) {
408 ownStyle.backgroundColor = color;
409 invalidate();
410 return this;
411 }
412 /// set background color for widget - from string like "#5599CC" or "white"
413 @property Widget backgroundColor(string colorString) {
414 uint color = decodeHexColor(colorString, COLOR_TRANSPARENT);
415 ownStyle.backgroundColor = color;
416 invalidate();
417 return this;
418 }
419
420 /// background image id
421 @property string backgroundImageId() const {
422 return style.backgroundImageId;
423 }
424
425 /// background image id
426 @property Widget backgroundImageId(string imageId) {
427 ownStyle.backgroundImageId = imageId;
428 return this;
429 }
430
431 /// returns colors to draw focus rectangle (one for solid, two for vertical gradient) or null if no focus rect should be drawn for style
432 @property const(uint[]) focusRectColors() const {
433 return style.focusRectColors;
434 }
435
436 DrawableRef _backgroundDrawable;
437 /// background drawable
438 @property DrawableRef backgroundDrawable() const {
439 if (_backgroundDrawable.isNull)
440 return stateStyle.backgroundDrawable;
441 return (cast(Widget)this)._backgroundDrawable;
442 }
443 /// background drawable
444 @property void backgroundDrawable(DrawableRef drawable) {
445 _backgroundDrawable = drawable;
446 }
447
448 /// widget drawing alpha value (0=opaque .. 255=transparent)
449 @property uint alpha() const { return stateStyle.alpha; }
450 /// set widget drawing alpha value (0=opaque .. 255=transparent)
451 @property Widget alpha(uint value) {
452 ownStyle.alpha = value;
453 invalidate();
454 return this;
455 }
456 /// get text color (ARGB 32 bit value)
457 @property uint textColor() const { return stateStyle.textColor; }
458 /// set text color (ARGB 32 bit value)
459 @property Widget textColor(uint value) {
460 ownStyle.textColor = value;
461 invalidate();
462 return this;
463 }
464 /// set text color for widget - from string like "#5599CC" or "white"
465 @property Widget textColor(string colorString) {
466 uint color = decodeHexColor(colorString, 0x000000);
467 ownStyle.textColor = color;
468 invalidate();
469 return this;
470 }
471
472
473 /// get text flags (bit set of TextFlag enum values)
474 @property uint textFlags() {
475 uint res = stateStyle.textFlags;
476 if (res == TEXT_FLAGS_USE_PARENT) {
477 if (parent)
478 res = parent.textFlags;
479 else
480 res = 0;
481 }
482 if (res & TextFlag.UnderlineHotKeysWhenAltPressed) {
483 uint modifiers = 0;
484 if (window !is null)
485 modifiers = window.keyboardModifiers;
486 bool altPressed = (modifiers & (KeyFlag.Alt | KeyFlag.LAlt | KeyFlag.RAlt)) != 0;
487 if (!altPressed) {
488 res = (res & ~(TextFlag.UnderlineHotKeysWhenAltPressed | TextFlag.UnderlineHotKeys)) | TextFlag.HotKeys;
489 } else {
490 res |= TextFlag.UnderlineHotKeys;
491 }
492 }
493
494 return res;
495 }
496 /// set text flags (bit set of TextFlag enum values)
497 @property Widget textFlags(uint value) {
498 ownStyle.textFlags = value;
499 bool oldHotkeys = (ownStyle.textFlags & (TextFlag.HotKeys | TextFlag.UnderlineHotKeys | TextFlag.UnderlineHotKeysWhenAltPressed)) != 0;
500 bool newHotkeys = (value & (TextFlag.HotKeys | TextFlag.UnderlineHotKeys | TextFlag.UnderlineHotKeysWhenAltPressed)) != 0;
501 handleFontChanged();
502 if (oldHotkeys != newHotkeys)
503 requestLayout();
504 else
505 invalidate();
506 return this;
507 }
508 /// returns font face
509 @property string fontFace() const { return stateStyle.fontFace; }
510 /// set font face for widget - override one from style
511 @property Widget fontFace(string face) {
512 ownStyle.fontFace = face;
513 handleFontChanged();
514 requestLayout();
515 return this;
516 }
517 /// returns font style (italic/normal)
518 @property bool fontItalic() const { return stateStyle.fontItalic; }
519 /// set font style (italic/normal) for widget - override one from style
520 @property Widget fontItalic(bool italic) {
521 ownStyle.fontStyle = italic ? FONT_STYLE_ITALIC : FONT_STYLE_NORMAL;
522 handleFontChanged();
523 requestLayout();
524 return this;
525 }
526 /// returns font weight
527 @property ushort fontWeight() const { return stateStyle.fontWeight; }
528 /// set font weight for widget - override one from style
529 @property Widget fontWeight(int weight) {
530 if (weight < 100)
531 weight = 100;
532 else if (weight > 900)
533 weight = 900;
534 ownStyle.fontWeight = cast(ushort)weight;
535 handleFontChanged();
536 requestLayout();
537 return this;
538 }
539 /// returns font size in pixels
540 @property int fontSize() const { return stateStyle.fontSize; }
541 /// set font size for widget - override one from style
542 @property Widget fontSize(int size) {
543 ownStyle.fontSize = size;
544 handleFontChanged();
545 requestLayout();
546 return this;
547 }
548 /// returns font family
549 @property FontFamily fontFamily() const { return stateStyle.fontFamily; }
550 /// set font family for widget - override one from style
551 @property Widget fontFamily(FontFamily family) {
552 ownStyle.fontFamily = family;
553 handleFontChanged();
554 requestLayout();
555 return this;
556 }
557 /// returns alignment (combined vertical and horizontal)
558 @property ubyte alignment() const { return style.alignment; }
559 /// sets alignment (combined vertical and horizontal)
560 @property Widget alignment(ubyte value) {
561 ownStyle.alignment = value;
562 requestLayout();
563 return this;
564 }
565 /// returns horizontal alignment
566 @property Align valign() { return cast(Align)(alignment & Align.VCenter); }
567 /// returns vertical alignment
568 @property Align halign() { return cast(Align)(alignment & Align.HCenter); }
569 /// returns font set for widget using style or set manually
570 @property FontRef font() const { return stateStyle.font; }
571
572 /// returns widget content text (override to support this)
573 @property dstring text() { return ""; }
574 /// sets widget content text (override to support this)
575 @property Widget text(dstring s) { return this; }
576 /// sets widget content text (override to support this)
577 @property Widget text(UIString s) { return this; }
578
579 /// override to handle font changes
580 protected void handleFontChanged() {}
581
582 //==================================================================
583 // Layout and drawing related methods
584
585 /// returns true if layout is required for widget and its children
586 @property bool needLayout() { return _needLayout; }
587 /// returns true if redraw is required for widget and its children
588 @property bool needDraw() { return _needDraw; }
589 /// returns true is widget is being animated - need to call animate() and redraw
590 @property bool animating() { return false; }
591 /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second)
592 void animate(long interval) {
593 }
594 /// returns measured width (calculated during measure() call)
595 @property measuredWidth() { return _measuredWidth; }
596 /// returns measured height (calculated during measure() call)
597 @property measuredHeight() { return _measuredHeight; }
598 /// returns current width of widget in pixels
599 @property int width() { return _pos.width; }
600 /// returns current height of widget in pixels
601 @property int height() { return _pos.height; }
602 /// returns widget rectangle top position
603 @property int top() { return _pos.top; }
604 /// returns widget rectangle left position
605 @property int left() { return _pos.left; }
606 /// returns widget rectangle
607 @property Rect pos() { return _pos; }
608 /// returns min width constraint
609 @property int minWidth() { return style.minWidth; }
610 /// returns max width constraint (SIZE_UNSPECIFIED if no constraint set)
611 @property int maxWidth() { return style.maxWidth; }
612 /// returns min height constraint
613 @property int minHeight() { return style.minHeight; }
614 /// returns max height constraint (SIZE_UNSPECIFIED if no constraint set)
615 @property int maxHeight() { return style.maxHeight; }
616
617 /// set max width constraint (SIZE_UNSPECIFIED for no constraint)
618 @property Widget maxWidth(int value) { ownStyle.maxWidth = value; return this; }
619 /// set max width constraint (0 for no constraint)
620 @property Widget minWidth(int value) { ownStyle.minWidth = value; return this; }
621 /// set max height constraint (SIZE_UNSPECIFIED for no constraint)
622 @property Widget maxHeight(int value) { ownStyle.maxHeight = value; return this; }
623 /// set max height constraint (0 for no constraint)
624 @property Widget minHeight(int value) { ownStyle.minHeight = value; return this; }
625
626 /// returns layout width options (WRAP_CONTENT, FILL_PARENT, some constant value or percent but only for one widget in layout)
627 @property int layoutWidth() { return style.layoutWidth; }
628 /// returns layout height options (WRAP_CONTENT, FILL_PARENT, some constant value or percent but only for one widget in layout)
629 @property int layoutHeight() { return style.layoutHeight; }
630 /// returns layout weight (while resizing to fill parent, widget will be resized proportionally to this value)
631 @property int layoutWeight() { return style.layoutWeight; }
632
633 /// sets layout width options (WRAP_CONTENT, FILL_PARENT, or some constant value)
634 @property Widget layoutWidth(int value) { ownStyle.layoutWidth = value; return this; }
635 /// sets layout height options (WRAP_CONTENT, FILL_PARENT, or some constant value)
636 @property Widget layoutHeight(int value) { ownStyle.layoutHeight = value; return this; }
637 /// sets layout weight (while resizing to fill parent, widget will be resized proportionally to this value)
638 @property Widget layoutWeight(int value) { ownStyle.layoutWeight = value; return this; }
639
640 /// returns widget visibility (Visible, Invisible, Gone)
641 @property Visibility visibility() { return _visibility; }
642 /// sets widget visibility (Visible, Invisible, Gone)
643 @property Widget visibility(Visibility visible) {
644 if (_visibility != visible) {
645 if ((_visibility == Visibility.Gone) || (visible == Visibility.Gone)) {
646 if (parent)
647 parent.requestLayout();
648 else
649 requestLayout();
650 } else
651 invalidate();
652 _visibility = visible;
653 }
654 return this;
655 }
656
657 /// returns true if point is inside of this widget
658 bool isPointInside(int x, int y) {
659 return _pos.isPointInside(x, y);
660 }
661
662 /// return true if state has State.Enabled flag set
663 @property bool enabled() { return (state & State.Enabled) != 0; }
664 /// change enabled state
665 @property Widget enabled(bool flg) { flg ? setState(State.Enabled) : resetState(State.Enabled); return this; }
666
667 protected bool _clickable;
668 /// when true, user can click this control, and get onClick listeners called
669 @property bool clickable() { return _clickable; }
670 @property Widget clickable(bool flg) { _clickable = flg; return this; }
671 @property bool canClick() { return _clickable && enabled && visible; }
672
673 protected bool _checkable;
674 /// when true, control supports Checked state
675 @property bool checkable() { return _checkable; }
676 @property Widget checkable(bool flg) { _checkable = flg; return this; }
677 @property bool canCheck() { return _checkable && enabled && visible; }
678
679
680 protected bool _checked;
681 /// get checked state
682 @property bool checked() { return (state & State.Checked) != 0; }
683 /// set checked state
684 @property Widget checked(bool flg) {
685 if (flg != checked) {
686 if (flg)
687 setState(State.Checked);
688 else
689 resetState(State.Checked);
690 invalidate();
691 }
692 return this;
693 }
694
695 protected bool _focusable;
696 /// whether widget can be focused
697 @property bool focusable() const { return _focusable; }
698 @property Widget focusable(bool flg) { _focusable = flg; return this; }
699
700 @property bool focused() const {
701 return (window !is null && window.focusedWidget is this && (state & State.Focused));
702 }
703
704 /// override and return true to track key events even when not focused
705 @property bool wantsKeyTracking() {
706 return false;
707 }
708
709 protected Action _action;
710 /// action to emit on click
711 @property const(Action) action() { return _action; }
712 /// action to emit on click
713 @property void action(const Action action) { _action = action.clone; handleActionStateChanged(); }
714 /// action to emit on click
715 @property void action(Action action) { _action = action; handleActionStateChanged(); }
716 /// ask for update state of some action (unles force=true, checks window flag actionsUpdateRequested), returns true if action state is changed
717 bool updateActionState(Action a, bool force = false, bool allowDefault = true) {
718 if (Window w = window) {
719 if (!force && !w.actionsUpdateRequested())
720 return false;
721 const ActionState oldState = a.state;
722 //import dlangui.widgets.editors;
723 //if (a.id == EditorActions.Undo) {
724 // Log.d("Requesting Undo action. Old state: ", a.state);
725 //}
726 if (w.dispatchActionStateRequest(a, this)) {
727 // state is updated
728 //Log.d("updateActionState ", a.label, " found state: ", a.state.toString);
729 if (allowDefault)
730 return true; // return 'request dispatched' flag instead of 'changed'
731 } else {
732 if (!allowDefault)
733 return false;
734 a.state = a.defaultState;
735 //Log.d("updateActionState ", a.label, " using default state: ", a.state.toString);
736 }
737 if (a.state != oldState)
738 return true;
739 }
740 return false;
741 }
742 /// call to update state for action (if action is assigned for widget)
743 void updateActionState(bool force = false) {
744 if (!_action || !(action.stateUpdateFlag & ActionStateUpdateFlag.inWidget))
745 return;
746 if (updateActionState(_action, force))
747 handleActionStateChanged();
748 }
749 /// called when state of action assigned on widget is changed
750 void handleActionStateChanged() {
751 // override to update enabled state, visibility and checked state
752 // default processing: copy flags to this widget
753 updateStateFromAction(_action);
754 }
755 /// apply enabled, visibile and checked state for this widget from action's state
756 void updateStateFromAction(Action a) {
757 const ActionState s = a.state;
758 if (s.enabled != enabled) {
759 enabled = s.enabled;
760 }
761 if (s.checked != checked) {
762 checked = s.checked;
763 }
764 bool v = _visibility == Visibility.Visible;
765 if (s.visible != v) {
766 visibility = s.visible ? Visibility.Visible : Visibility.Gone;
767 }
768 }
769 /// set action update request flag, will be cleared after redraw
770 void requestActionsUpdate(bool immediateUpdate = false) {
771 if (Window w = window) {
772 w.requestActionsUpdate(immediateUpdate);
773 }
774 }
775
776 protected UIString _tooltipText;
777 /// tooltip text - when not empty, widget will show tooltips automatically; for advanced tooltips - override hasTooltip and createTooltip
778 @property dstring tooltipText() { return _tooltipText; }
779 /// tooltip text - when not empty, widget will show tooltips automatically; for advanced tooltips - override hasTooltip and createTooltip
780 @property Widget tooltipText(dstring text) { _tooltipText = text; return this; }
781 /// tooltip text - when not empty, widget will show tooltips automatically; for advanced tooltips - override hasTooltip and createTooltip
782 @property Widget tooltipText(UIString text) { _tooltipText = text; return this; }
783
784
785 /// returns true if widget has tooltip to show
786 @property bool hasTooltip() {
787 return tooltipText.length > 0;
788 }
789 /// will be called from window once tooltip request timer expired; if null is returned, popup will not be shown; you can change alignment and position of popup here
790 Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) {
791 // default implementation supports tooltips when tooltipText property is set
792 if (!_tooltipText.empty) {
793 import dlangui.widgets.controls;
794 Widget res = new TextWidget("tooltip", _tooltipText.value);
795 res.styleId = STYLE_TOOLTIP;
796 return res;
797 }
798 return null;
799 }
800
801 /// schedule tooltip
802 void scheduleTooltip(long delay = 300, uint alignment = 2 /*PopupAlign.Below*/, int x = 0, int y = 0) {
803 if (auto w = window)
804 w.scheduleTooltip(this, delay, alignment, x, y);
805 }
806
807 protected bool _focusGroup;
808 /*****************************************
809 * When focus group is set for some parent widget, focus from one of containing widgets can be moved using keyboard only to one of other widgets containing in it and cannot bypass bounds of focusGroup.
810 *
811 * If focused widget doesn't have any parent with focusGroup == true, focus may be moved to any focusable within window.
812 *
813 */
814 @property bool focusGroup() { return _focusGroup; }
815 /// set focus group flag for container widget
816 @property Widget focusGroup(bool flg) { _focusGroup = flg; return this; }
817 @property bool focusGroupFocused() const {
818 Widget w = focusGroupWidget();
819 return (w._state & State.WindowFocused) != 0;
820 }
821 protected bool setWindowFocusedFlag(bool flg) {
822 if (flg) {
823 if ((_state & State.WindowFocused) == 0) {
824 _state |= State.WindowFocused;
825 invalidate();
826 return true;
827 }
828 } else {
829 if ((_state & State.WindowFocused) != 0) {
830 _state &= ~State.WindowFocused;
831 invalidate();
832 return true;
833 }
834 }
835 return false;
836 }
837 @property Widget focusGroupFocused(bool flg) {
838 Widget w = focusGroupWidget();
839 w.setWindowFocusedFlag(flg);
840 while (w.parent) {
841 w = w.parent;
842 if (w.parent is null || w.focusGroup) {
843 w.setWindowFocusedFlag(flg);
844 }
845 }
846 return this;
847 }
848
849 /// find nearest parent of this widget with focusGroup flag, returns topmost parent if no focusGroup flag set to any of parents.
850 Widget focusGroupWidget() inout {
851 Widget p = cast(Widget)this;
852 while (p) {
853 if (!p.parent || p.focusGroup)
854 break;
855 p = p.parent;
856 }
857 return p;
858 }
859
860 private static class TabOrderInfo {
861 Widget widget;
862 uint tabOrder;
863 uint childOrder;
864 Rect rect;
865 this(Widget widget, Rect rect) {
866 this.widget = widget;
867 this.tabOrder = widget.thisOrParentTabOrder();
868 this.rect = widget.pos;
869 }
870 static if (BACKEND_GUI) {
871 static immutable int NEAR_THRESHOLD = 10;
872 } else {
873 static immutable int NEAR_THRESHOLD = 1;
874 }
875 bool nearX(TabOrderInfo v) {
876 return v.rect.left >= rect.left - NEAR_THRESHOLD && v.rect.left <= rect.left + NEAR_THRESHOLD;
877 }
878 bool nearY(TabOrderInfo v) {
879 return v.rect.top >= rect.top - NEAR_THRESHOLD && v.rect.top <= rect.top + NEAR_THRESHOLD;
880 }
881 override int opCmp(Object obj) const {
882 TabOrderInfo v = cast(TabOrderInfo)obj;
883 if (tabOrder != 0 && v.tabOrder !=0) {
884 if (tabOrder < v.tabOrder)
885 return -1;
886 if (tabOrder > v.tabOrder)
887 return 1;
888 }
889 // place items with tabOrder 0 after items with tabOrder non-0
890 if (tabOrder != 0)
891 return -1;
892 if (v.tabOrder != 0)
893 return 1;
894 if (childOrder < v.childOrder)
895 return -1;
896 if (childOrder > v.childOrder)
897 return 1;
898 return 0;
899 }
900 /// less predicat for Left/Right sorting
901 static bool lessHorizontal(TabOrderInfo obj1, TabOrderInfo obj2) {
902 if (obj1.nearY(obj2)) {
903 return obj1.rect.left < obj2.rect.left;
904 }
905 return obj1.rect.top < obj2.rect.top;
906 }
907 /// less predicat for Up/Down sorting
908 static bool lessVertical(TabOrderInfo obj1, TabOrderInfo obj2) {
909 if (obj1.nearX(obj2)) {
910 return obj1.rect.top < obj2.rect.top;
911 }
912 return obj1.rect.left < obj2.rect.left;
913 }
914 override string toString() const {
915 return widget.id;
916 }
917 }
918
919 private void findFocusableChildren(ref TabOrderInfo[] results, Rect clipRect, Widget currentWidget) {
920 if (visibility != Visibility.Visible)
921 return;
922 Rect rc = _pos;
923 applyMargins(rc);
924 applyPadding(rc);
925 if (!rc.intersects(clipRect))
926 return; // out of clip rectangle
927 if (canFocus || this is currentWidget) {
928 TabOrderInfo item = new TabOrderInfo(this, rc);
929 results ~= item;
930 return;
931 }
932 rc.intersect(clipRect);
933 for (int i = 0; i < childCount(); i++) {
934 child(i).findFocusableChildren(results, rc, currentWidget);
935 }
936 }
937
938 /// find all focusables belonging to the same focusGroup as this widget (does not include current widget).
939 /// usually to be called for focused widget to get possible alternatives to navigate to
940 private TabOrderInfo[] findFocusables(Widget currentWidget) {
941 TabOrderInfo[] result;
942 Widget group = focusGroupWidget();
943 group.findFocusableChildren(result, group.pos, currentWidget);
944 for (ushort i = 0; i < result.length; i++)
945 result[i].childOrder = i + 1;
946 sort(result);
947 return result;
948 }
949
950 protected ushort _tabOrder;
951 /// tab order - hint for focus movement using Tab/Shift+Tab
952 @property ushort tabOrder() { return _tabOrder; }
953 @property Widget tabOrder(ushort tabOrder) { _tabOrder = tabOrder; return this; }
954 private int thisOrParentTabOrder() {
955 if (_tabOrder)
956 return _tabOrder;
957 if (!parent)
958 return 0;
959 return parent.thisOrParentTabOrder;
960 }
961
962 /// call on focused widget, to find best
963 private Widget findNextFocusWidget(FocusMovement direction) {
964 if (direction == FocusMovement.None)
965 return this;
966 TabOrderInfo[] focusables = findFocusables(this);
967 if (!focusables.length)
968 return null;
969 int myIndex = -1;
970 for (int i = 0; i < focusables.length; i++) {
971 if (focusables[i].widget is this) {
972 myIndex = i;
973 break;
974 }
975 }
976 debug(DebugFocus) Log.d("findNextFocusWidget myIndex=", myIndex, " of focusables: ", focusables);
977 if (myIndex == -1)
978 return null; // not found myself
979 if (focusables.length == 1)
980 return focusables[0].widget; // single option - use it
981 if (direction == FocusMovement.Next) {
982 // move forward
983 int index = myIndex + 1;
984 if (index >= focusables.length)
985 index = 0;
986 return focusables[index].widget;
987 } else if (direction == FocusMovement.Previous) {
988 // move back
989 int index = myIndex - 1;
990 if (index < 0)
991 index = cast(int)focusables.length - 1;
992 return focusables[index].widget;
993 } else {
994 // Left, Right, Up, Down
995 if (direction == FocusMovement.Left || direction == FocusMovement.Right) {
996 sort!(TabOrderInfo.lessHorizontal)(focusables);
997 } else {
998 sort!(TabOrderInfo.lessVertical)(focusables);
999 }
1000 myIndex = 0;
1001 for (int i = 0; i < focusables.length; i++) {
1002 if (focusables[i].widget is this) {
1003 myIndex = i;
1004 break;
1005 }
1006 }
1007 int index = myIndex;
1008 if (direction == FocusMovement.Left || direction == FocusMovement.Up) {
1009 index--;
1010 if (index < 0)
1011 index = cast(int)focusables.length - 1;
1012 } else {
1013 index++;
1014 if (index >= focusables.length)
1015 index = 0;
1016 }
1017 return focusables[index].widget;
1018 }
1019 }
1020
1021 bool handleMoveFocusUsingKeys(KeyEvent event) {
1022 if (!focused || !visible)
1023 return false;
1024 if (event.action != KeyAction.KeyDown)
1025 return false;
1026 FocusMovement direction = FocusMovement.None;
1027 uint flags = event.flags & (KeyFlag.Shift | KeyFlag.Control | KeyFlag.Alt);
1028 switch (event.keyCode) with(KeyCode)
1029 {
1030 case LEFT:
1031 if (flags == 0)
1032 direction = FocusMovement.Left;
1033 break;
1034 case RIGHT:
1035 if (flags == 0)
1036 direction = FocusMovement.Right;
1037 break;
1038 case UP:
1039 if (flags == 0)
1040 direction = FocusMovement.Up;
1041 break;
1042 case DOWN:
1043 if (flags == 0)
1044 direction = FocusMovement.Down;
1045 break;
1046 case TAB:
1047 if (flags == 0)
1048 direction = FocusMovement.Next;
1049 else if (flags == KeyFlag.Shift)
1050 direction = FocusMovement.Previous;
1051 break;
1052 default:
1053 break;
1054 }
1055 if (direction == FocusMovement.None)
1056 return false;
1057 Widget nextWidget = findNextFocusWidget(direction);
1058 if (!nextWidget)
1059 return false;
1060 nextWidget.setFocus(FocusReason.TabFocus);
1061 return true;
1062 }
1063
1064 /// returns true if this widget and all its parents are visible
1065 @property bool visible() {
1066 if (visibility != Visibility.Visible)
1067 return false;
1068 if (parent is null)
1069 return true;
1070 return parent.visible;
1071 }
1072
1073 /// returns true if widget is focusable and visible and enabled
1074 @property bool canFocus() {
1075 return focusable && visible && enabled;
1076 }
1077
1078 /// sets focus to this widget or suitable focusable child, returns previously focused widget
1079 Widget setFocus(FocusReason reason = FocusReason.Unspecified) {
1080 if (window is null)
1081 return null;
1082 if (!visible)
1083 return window.focusedWidget;
1084 invalidate();
1085 if (!canFocus) {
1086 Widget w = findFocusableChild(true);
1087 if (!w)
1088 w = findFocusableChild(false);
1089 if (w)
1090 return window.setFocus(w, reason);
1091 // try to find focusable child
1092 return window.focusedWidget;
1093 }
1094 return window.setFocus(this, reason);
1095 }
1096 /// searches children for first focusable item, returns null if not found
1097 Widget findFocusableChild(bool defaultOnly) {
1098 for(int i = 0; i < childCount; i++) {
1099 Widget w = child(i);
1100 if (w.canFocus && (!defaultOnly || (w.state & State.Default) != 0))
1101 return w;
1102 w = w.findFocusableChild(defaultOnly);
1103 if (w !is null)
1104 return w;
1105 }
1106 if (canFocus)
1107 return this;
1108 return null;
1109 }
1110
1111 // =======================================================
1112 // Events
1113
1114 protected ActionMap _acceleratorMap;
1115 @property ref ActionMap acceleratorMap() { return _acceleratorMap; }
1116
1117 /// override to handle specific actions
1118 bool handleAction(const Action a) {
1119 if (onAction.assigned)
1120 if (onAction(this, a))
1121 return true;
1122 return false;
1123 }
1124 /// override to handle specific actions state (e.g. change enabled state for supported actions)
1125 bool handleActionStateRequest(const Action a) {
1126 return false;
1127 }
1128
1129 /// call to dispatch action
1130 bool dispatchAction(const Action a) {
1131 if (window)
1132 return window.dispatchAction(a, this);
1133 else
1134 return handleAction(a);
1135 }
1136
1137 // called to process click and notify listeners
1138 protected bool handleClick() {
1139 bool res = false;
1140 if (click.assigned)
1141 res = click(this);
1142 else if (_action) {
1143 return dispatchAction(_action);
1144 }
1145 return res;
1146 }
1147
1148
1149 void cancelLayout() {
1150 _needLayout = false;
1151 }
1152
1153 /// set new timer to call onTimer() after specified interval (for recurred notifications, return true from onTimer)
1154 ulong setTimer(long intervalMillis) {
1155 if (auto w = window)
1156 return w.setTimer(this, intervalMillis);
1157 return 0; // no window - no timer
1158 }
1159
1160 /// cancel timer - pass value returned from setTimer() as timerId parameter
1161 void cancelTimer(ulong timerId) {
1162 if (auto w = window)
1163 w.cancelTimer(timerId);
1164 }
1165
1166 /// handle timer; return true to repeat timer event after next interval, false cancel timer
1167 bool onTimer(ulong id) {
1168 // override to do something useful
1169 // return true to repeat after the same interval, false to stop timer
1170 return false;
1171 }
1172
1173 /// map key to action
1174 Action findKeyAction(uint keyCode, uint flags) {
1175 Action action = _acceleratorMap.findByKey(keyCode, flags);
1176 if (action)
1177 return action;
1178 if (keyToAction.assigned)
1179 action = keyToAction(this, keyCode, flags);
1180 return action;
1181 }
1182
1183 /// process key event, return true if event is processed.
1184 bool onKeyEvent(KeyEvent event) {
1185 if (keyEvent.assigned && keyEvent(this, event))
1186 return true; // processed by external handler
1187 if (event.action == KeyAction.KeyDown) {
1188 //Log.d("Find key action for key = ", event.keyCode, " flags=", event.flags);
1189 Action action = findKeyAction(event.keyCode, event.flags); // & (KeyFlag.Shift | KeyFlag.Alt | KeyFlag.Control | KeyFlag.Menu)
1190 if (action !is null) {
1191 //Log.d("Action found: ", action.id, " ", action.labelValue.id);
1192 // update action state
1193 if ((action.stateUpdateFlag & ActionStateUpdateFlag.inAccelerator) && updateActionState(action, true) && action is _action)
1194 handleActionStateChanged();
1195
1196 //run only enabled actions
1197 if (action.state.enabled)
1198 return dispatchAction(action);
1199 }
1200 }
1201 // handle focus navigation using keys
1202 if (focused && handleMoveFocusUsingKeys(event))
1203 return true;
1204 if (canClick) {
1205 // support onClick event initiated by Space or Return keys
1206 if (event.action == KeyAction.KeyDown) {
1207 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) {
1208 setState(State.Pressed);
1209 return true;
1210 }
1211 }
1212 if (event.action == KeyAction.KeyUp) {
1213 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) {
1214 resetState(State.Pressed);
1215 handleClick();
1216 return true;
1217 }
1218 }
1219 }
1220 return false;
1221 }
1222
1223 /// handle custom event
1224 bool onEvent(CustomEvent event) {
1225 RunnableEvent runnable = cast(RunnableEvent)event;
1226 if (runnable) {
1227 // handle runnable
1228 runnable.run();
1229 return true;
1230 }
1231 // override to handle more events
1232 return false;
1233 }
1234
1235 /// execute delegate later in UI thread if this widget will be still available (can be used to modify UI from background thread, or just to postpone execution of action)
1236 void executeInUiThread(void delegate() runnable) {
1237 if (!window)
1238 return;
1239 RunnableEvent event = new RunnableEvent(CUSTOM_RUNNABLE, this, runnable);
1240 window.postEvent(event);
1241 }
1242
1243 /// process mouse event; return true if event is processed by widget.
1244 bool onMouseEvent(MouseEvent event) {
1245 if (mouseEvent.assigned && mouseEvent(this, event))
1246 return true; // processed by external handler
1247 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")");
1248 // support onClick
1249 if (canClick) {
1250 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
1251 setState(State.Pressed);
1252 if (canFocus)
1253 setFocus();
1254 return true;
1255 }
1256 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
1257 resetState(State.Pressed);
1258 handleClick();
1259 return true;
1260 }
1261 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
1262 resetState(State.Pressed);
1263 resetState(State.Hovered);
1264 return true;
1265 }
1266 if (event.action == MouseAction.FocusIn) {
1267 setState(State.Pressed);
1268 return true;
1269 }
1270 }
1271 if (event.action == MouseAction.Move && !event.hasModifiers && hasTooltip) {
1272 scheduleTooltip(200);
1273 }
1274 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Right) {
1275 if (canShowPopupMenu(event.x, event.y)) {
1276 showPopupMenu(event.x, event.y);
1277 return true;
1278 }
1279 }
1280 if (canFocus && event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
1281 setFocus();
1282 return true;
1283 }
1284 if (trackHover) {
1285 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
1286 if ((state & State.Hovered)) {
1287 debug(mouse) Log.d("Hover off ", id);
1288 resetState(State.Hovered);
1289 }
1290 return true;
1291 }
1292 if (event.action == MouseAction.Move) {
1293 if (!(state & State.Hovered)) {
1294 debug(mouse) Log.d("Hover ", id);
1295 if (!TOUCH_MODE)
1296 setState(State.Hovered);
1297 }
1298 return true;
1299 }
1300 if (event.action == MouseAction.Leave) {
1301 debug(mouse) Log.d("Leave ", id);
1302 resetState(State.Hovered);
1303 return true;
1304 }
1305 }
1306 return false;
1307 }
1308
1309 // =======================================================
1310 // Signals
1311
1312 /// on click event listener (bool delegate(Widget))
1313 Signal!OnClickHandler click;
1314
1315 /// checked state change event listener (bool delegate(Widget, bool))
1316 Signal!OnCheckHandler checkChange;
1317
1318 /// focus state change event listener (bool delegate(Widget, bool))
1319 Signal!OnFocusHandler focusChange;
1320
1321 /// key event listener (bool delegate(Widget, KeyEvent)) - return true if event is processed by handler
1322 Signal!OnKeyHandler keyEvent;
1323
1324 /// action by key lookup handler
1325 Listener!OnKeyActionHandler keyToAction;
1326
1327 /// action handlers
1328 Signal!OnActionHandler onAction;
1329
1330 /// mouse event listener (bool delegate(Widget, MouseEvent)) - return true if event is processed by handler
1331 Signal!OnMouseHandler mouseEvent;
1332
1333
1334 // Signal utils
1335
1336 /// helper function to add onCheckChangeListener in method chain
1337 Widget addOnClickListener(bool delegate(Widget) listener) {
1338 click.connect(listener);
1339 return this;
1340 }
1341
1342 /// helper function to add onCheckChangeListener in method chain
1343 Widget addOnCheckChangeListener(bool delegate(Widget, bool) listener) {
1344 checkChange.connect(listener);
1345 return this;
1346 }
1347
1348 /// helper function to add onFocusChangeListener in method chain
1349 Widget addOnFocusChangeListener(bool delegate(Widget, bool) listener) {
1350 focusChange.connect(listener);
1351 return this;
1352 }
1353
1354 // =======================================================
1355 // Layout and measurement methods
1356
1357 /// request relayout of widget and its children
1358 void requestLayout() {
1359 _needLayout = true;
1360 }
1361 /// request redraw
1362 void invalidate() {
1363 _needDraw = true;
1364 }
1365
1366 /// helper function for implement measure() when widget's content dimensions are known
1367 protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) {
1368 if (visibility == Visibility.Gone) {
1369 _measuredWidth = _measuredHeight = 0;
1370 return;
1371 }
1372 Rect m = margins;
1373 Rect p = padding;
1374 // summarize margins, padding, and content size
1375 int dx = m.left + m.right + p.left + p.right + contentWidth;
1376 int dy = m.top + m.bottom + p.top + p.bottom + contentHeight;
1377 // check for fixed size set in layoutWidth, layoutHeight
1378 int lh = layoutHeight;
1379 int lw = layoutWidth;
1380 // constant value support
1381 if (!(isPercentSize(lh) || isSpecialSize(lh)))
1382 dy = lh.toPixels();
1383 if (!(isPercentSize(lw) || isSpecialSize(lw)))
1384 dx = lw.toPixels();
1385 // apply min/max width and height constraints
1386 int minw = minWidth;
1387 int maxw = maxWidth;
1388 int minh = minHeight;
1389 int maxh = maxHeight;
1390 if (minw != SIZE_UNSPECIFIED && dx < minw)
1391 dx = minw;
1392 if (minh != SIZE_UNSPECIFIED && dy < minh)
1393 dy = minh;
1394 if (maxw != SIZE_UNSPECIFIED && dx > maxw)
1395 dx = maxw;
1396 if (maxh != SIZE_UNSPECIFIED && dy > maxh)
1397 dy = maxh;
1398 // apply FILL_PARENT
1399 //if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT)
1400 // dx = parentWidth;
1401 //if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT)
1402 // dy = parentHeight;
1403 // apply max parent size constraint
1404 if (parentWidth != SIZE_UNSPECIFIED && dx > parentWidth)
1405 dx = parentWidth;
1406 if (parentHeight != SIZE_UNSPECIFIED && dy > parentHeight)
1407 dy = parentHeight;
1408 _measuredWidth = dx;
1409 _measuredHeight = dy;
1410 }
1411
1412 /**
1413 Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
1414
1415 */
1416 void measure(int parentWidth, int parentHeight) {
1417 measuredContent(parentWidth, parentHeight, 0, 0);
1418 }
1419
1420 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
1421 void layout(Rect rc) {
1422 if (visibility == Visibility.Gone) {
1423 return;
1424 }
1425 _pos = rc;
1426 _needLayout = false;
1427 }
1428
1429 /// draws focus rectangle, if enabled in styles
1430 void drawFocusRect(DrawBuf buf, Rect rc) {
1431 const uint[] colors = focusRectColors;
1432 if (colors) {
1433 buf.drawFocusRect(rc, colors);
1434 }
1435 }
1436
1437 /// Draw widget at its position to buffer
1438 void onDraw(DrawBuf buf) {
1439 if (visibility != Visibility.Visible)
1440 return;
1441 Rect rc = _pos;
1442 applyMargins(rc);
1443 auto saver = ClipRectSaver(buf, rc, alpha);
1444 DrawableRef bg = backgroundDrawable;
1445 if (!bg.isNull) {
1446 bg.drawTo(buf, rc, state);
1447 }
1448 applyPadding(rc);
1449 if (state & State.Focused) {
1450 rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
1451 drawFocusRect(buf, rc);
1452 }
1453 _needDraw = false;
1454 }
1455
1456 /// Helper function: applies margins to rectangle
1457 void applyMargins(ref Rect rc) {
1458 Rect m = margins;
1459 rc.left += m.left;
1460 rc.top += m.top;
1461 rc.bottom -= m.bottom;
1462 rc.right -= m.right;
1463 }
1464 /// Helper function: applies padding to rectangle
1465 void applyPadding(ref Rect rc) {
1466 Rect m = padding;
1467 rc.left += m.left;
1468 rc.top += m.top;
1469 rc.bottom -= m.bottom;
1470 rc.right -= m.right;
1471 }
1472 /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
1473 static void applyAlign(ref Rect rc, Point sz, Align ha, Align va) {
1474 if (va == Align.Bottom) {
1475 rc.top = rc.bottom - sz.y;
1476 } else if (va == Align.VCenter) {
1477 int dy = (rc.height - sz.y) / 2;
1478 rc.top += dy;
1479 rc.bottom = rc.top + sz.y;
1480 } else {
1481 rc.bottom = rc.top + sz.y;
1482 }
1483 if (ha == Align.Right) {
1484 rc.left = rc.right - sz.x;
1485 } else if (ha == Align.HCenter) {
1486 int dx = (rc.width - sz.x) / 2;
1487 rc.left += dx;
1488 rc.right = rc.left + sz.x;
1489 } else {
1490 rc.right = rc.left + sz.x;
1491 }
1492 }
1493 /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
1494 void applyAlign(ref Rect rc, Point sz) {
1495 Align va = valign;
1496 Align ha = halign;
1497 applyAlign(rc, sz, ha, va);
1498 }
1499
1500 // ===========================================================
1501 // popup menu support
1502 /// returns true if widget can show popup menu (e.g. by mouse right click at point x,y)
1503 bool canShowPopupMenu(int x, int y) {
1504 return false;
1505 }
1506 /// shows popup menu at (x,y)
1507 void showPopupMenu(int x, int y) {
1508 // override to show popup
1509 }
1510 /// override to change popup menu items state
1511 bool isActionEnabled(const Action action) {
1512 return true;
1513 }
1514
1515 // ===========================================================
1516 // Widget hierarhy methods
1517
1518 /// returns number of children of this widget
1519 @property int childCount() { return 0; }
1520 /// returns child by index
1521 Widget child(int index) { return null; }
1522 /// adds child, returns added item
1523 Widget addChild(Widget item) { assert(false, "addChild: children not suported for this widget type"); }
1524 /// adds child, returns added item
1525 Widget addChildren(Widget[] items) {
1526 foreach(item; items) {
1527 addChild(item);
1528 }
1529 return this;
1530 }
1531 /// inserts child at given index, returns inserted item
1532 Widget insertChild(Widget item, int index) {assert(false, "insertChild: children not suported for this widget type"); }
1533 /// removes child, returns removed item
1534 Widget removeChild(int index) { assert(false, "removeChild: children not suported for this widget type"); }
1535 /// removes child by ID, returns removed item
1536 Widget removeChild(string id) { assert(false, "removeChild: children not suported for this widget type"); }
1537 /// removes child, returns removed item
1538 Widget removeChild(Widget child) { assert(false, "removeChild: children not suported for this widget type"); }
1539 /// returns index of widget in child list, -1 if passed widget is not a child of this widget
1540 int childIndex(Widget item) { return -1; }
1541
1542
1543 /// returns true if item is child of this widget (when deepSearch == true - returns true if item is this widget or one of children inside children tree).
1544 bool isChild(Widget item, bool deepSearch = true) {
1545 if (deepSearch) {
1546 // this widget or some widget inside children tree
1547 if (item is this)
1548 return true;
1549 for (int i = 0; i < childCount; i++) {
1550 if (child(i).isChild(item))
1551 return true;
1552 }
1553 } else {
1554 // only one of children
1555 for (int i = 0; i < childCount; i++) {
1556 if (item is child(i))
1557 return true;
1558 }
1559 }
1560 return false;
1561 }
1562
1563 /// find child of specified type T by id, returns null if not found or cannot be converted to type T
1564 T childById(T = typeof(this))(string id, bool deepSearch = true) {
1565 if (deepSearch) {
1566 // search everywhere inside child tree
1567 if (compareId(id)) {
1568 T found = cast(T)this;
1569 if (found)
1570 return found;
1571 }
1572 // lookup children
1573 for (int i = childCount - 1; i >= 0; i--) {
1574 Widget res = child(i).childById(id);
1575 if (res !is null) {
1576 T found = cast(T)res;
1577 if (found)
1578 return found;
1579 }
1580 }
1581 } else {
1582 // search only across children of this widget
1583 for (int i = childCount - 1; i >= 0; i--) {
1584 Widget w = child(i);
1585 if (id.equal(w.id)) {
1586 T found = cast(T)w;
1587 if (found)
1588 return found;
1589 }
1590 }
1591 }
1592 // not found
1593 return null;
1594 }
1595
1596 /// returns parent widget, null for top level widget
1597 @property Widget parent() const { return _parent ? cast(Widget)_parent : null; }
1598 /// sets parent for widget
1599 @property Widget parent(Widget parent) { _parent = parent; return this; }
1600 /// returns window (if widget or its parent is attached to window)
1601 @property Window window() const {
1602 Widget p = cast(Widget)this;
1603 while (p !is null) {
1604 if (p._window !is null)
1605 return cast(Window)p._window;
1606 p = p.parent;
1607 }
1608 return null;
1609 }
1610 /// sets window (to be used for top level widget from Window implementation). TODO: hide it from API?
1611 @property void window(Window window) {
1612 _window = window;
1613 }
1614
1615 void removeAllChildren(bool destroyObj = true) {
1616 // override
1617 }
1618
1619 /// set string property value, for ML loaders
1620 bool setStringProperty(string name, string value) {
1621 mixin(generatePropertySetters("id", "styleId", "backgroundImageId", "backgroundColor", "textColor", "fontFace"));
1622 if (name.equal("text")) {
1623 text = UIString.fromId(value);
1624 return true;
1625 }
1626 if (name.equal("tooltipText")) {
1627 tooltipText = UIString.fromId(value);
1628 return true;
1629 }
1630 return false;
1631 }
1632
1633 /// set string property value, for ML loaders
1634 bool setDstringProperty(string name, dstring value) {
1635 if (name.equal("text")) {
1636 text = UIString.fromRaw(value);
1637 return true;
1638 }
1639 if (name.equal("tooltipText")) {
1640 tooltipText = UIString.fromRaw(value);
1641 return true;
1642 }
1643 return false;
1644 }
1645
1646 /// set string property value, for ML loaders
1647 bool setUistringProperty(string name, UIString value) {
1648 if (name.equal("text")) {
1649 text = value;
1650 return true;
1651 }
1652 if (name.equal("tooltipText")) {
1653 tooltipText = value;
1654 return true;
1655 }
1656 return false;
1657 }
1658
1659 /// StringListValue list values
1660 bool setStringListValueListProperty(string propName, StringListValue[] values) {
1661 return false;
1662 }
1663
1664 /// UIString list values
1665 bool setUIStringListProperty(string propName, UIString[] values) {
1666 return false;
1667 }
1668
1669 /// set string property value, for ML loaders
1670 bool setBoolProperty(string name, bool value) {
1671 mixin(generatePropertySetters("enabled", "clickable", "checkable", "focusable", "checked", "fontItalic"));
1672 return false;
1673 }
1674
1675 /// set double property value, for ML loaders
1676 bool setDoubleProperty(string name, double value) {
1677 if (name.equal("alpha")) {
1678 int n = cast(int)(value * 255);
1679 return setIntProperty(name, n);
1680 }
1681 return false;
1682 }
1683
1684 /// set int property value, for ML loaders
1685 bool setIntProperty(string name, int value) {
1686 if (name.equal("alpha")) {
1687 if (value < 0)
1688 value = 0;
1689 else if (value > 255)
1690 value = 255;
1691 alpha = cast(ushort)value;
1692 return true;
1693 }
1694 mixin(generatePropertySetters("minWidth", "maxWidth", "minHeight", "maxHeight", "layoutWidth", "layoutHeight", "layoutWeight", "textColor", "backgroundColor", "fontSize", "fontWeight"));
1695 if (name.equal("margins")) { // use same value for all sides
1696 margins = Rect(value, value, value, value);
1697 return true;
1698 }
1699 if (name.equal("alignment")) {
1700 alignment = cast(Align)value;
1701 return true;
1702 }
1703 if (name.equal("padding")) { // use same value for all sides
1704 padding = Rect(value, value, value, value);
1705 return true;
1706 }
1707 return false;
1708 }
1709
1710 /// set Rect property value, for ML loaders
1711 bool setRectProperty(string name, Rect value) {
1712 mixin(generatePropertySetters("margins", "padding"));
1713 return false;
1714 }
1715 }
1716
1717 /** Widget list holder. */
1718 alias WidgetList = ObjectList!Widget;
1719
1720 /** Base class for widgets which have children. */
1721 class WidgetGroup : Widget {
1722
1723 /// empty parameter list constructor - for usage by factory
1724 this() {
1725 this(null);
1726 }
1727 /// create with ID parameter
1728 this(string ID) {
1729 super(ID);
1730 }
1731
1732 protected WidgetList _children;
1733
1734 /// returns number of children of this widget
1735 @property override int childCount() { return _children.count; }
1736 /// returns child by index
1737 override Widget child(int index) { return _children.get(index); }
1738 /// adds child, returns added item
1739 override Widget addChild(Widget item) { return _children.add(item).parent(this); }
1740 /// inserts child at given index, returns inserted item
1741 override Widget insertChild(Widget item, int index) { return _children.insert(item,index).parent(this); }
1742 /// removes child, returns removed item
1743 override Widget removeChild(int index) {
1744 Widget res = _children.remove(index);
1745 if (res !is null)
1746 res.parent = null;
1747 return res;
1748 }
1749 /// removes child by ID, returns removed item
1750 override Widget removeChild(string ID) {
1751 Widget res = null;
1752 int index = _children.indexOf(ID);
1753 if (index < 0)
1754 return null;
1755 res = _children.remove(index);
1756 if (res !is null)
1757 res.parent = null;
1758 return res;
1759 }
1760 /// removes child, returns removed item
1761 override Widget removeChild(Widget child) {
1762 Widget res = null;
1763 int index = _children.indexOf(child);
1764 if (index < 0)
1765 return null;
1766 res = _children.remove(index);
1767 if (res !is null)
1768 res.parent = null;
1769 return res;
1770 }
1771 /// returns index of widget in child list, -1 if passed widget is not a child of this widget
1772 override int childIndex(Widget item) { return _children.indexOf(item); }
1773
1774 override void removeAllChildren(bool destroyObj = true) {
1775 _children.clear(destroyObj);
1776 }
1777
1778 /// replace child with other child
1779 void replaceChild(Widget newChild, Widget oldChild) {
1780 _children.replace(newChild, oldChild);
1781 }
1782
1783 }
1784
1785 /** WidgetGroup with default drawing of children (just draw all children) */
1786 class WidgetGroupDefaultDrawing : WidgetGroup {
1787 /// empty parameter list constructor - for usage by factory
1788 this() {
1789 this(null);
1790 }
1791 /// create with ID parameter
1792 this(string ID) {
1793 super(ID);
1794 }
1795 /// Draw widget at its position to buffer
1796 override void onDraw(DrawBuf buf) {
1797 if (visibility != Visibility.Visible)
1798 return;
1799 super.onDraw(buf);
1800 Rect rc = _pos;
1801 applyMargins(rc);
1802 applyPadding(rc);
1803 auto saver = ClipRectSaver(buf, rc);
1804 for (int i = 0; i < _children.count; i++) {
1805 Widget item = _children.get(i);
1806 item.onDraw(buf);
1807 }
1808 }
1809 }
1810
1811 /// helper for locating items in list, tree, table or other controls by typing their name
1812 struct TextTypingShortcutHelper {
1813 int timeoutMillis = 800; // expiration time for entered text; after timeout collected text will be cleared
1814 private long _lastUpdateTimeStamp;
1815 private dchar[] _text;
1816 /// cancel text collection (next typed text will be collected from scratch)
1817 void cancel() {
1818 _text.length = 0;
1819 _lastUpdateTimeStamp = 0;
1820 }
1821 /// returns collected text string - use it for lookup
1822 @property dstring text() { return _text.dup; }
1823 /// pass key event here; returns true if search text is updated and you can move selection using it
1824 bool onKeyEvent(KeyEvent event) {
1825 long ts = currentTimeMillis;
1826 if (_lastUpdateTimeStamp && ts - _lastUpdateTimeStamp > timeoutMillis)
1827 cancel();
1828 if (event.action == KeyAction.Text) {
1829 _text ~= event.text;
1830 _lastUpdateTimeStamp = ts;
1831 return _text.length > 0;
1832 }
1833 if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
1834 switch (event.keyCode) with (KeyCode) {
1835 case LEFT:
1836 case RIGHT:
1837 case UP:
1838 case DOWN:
1839 case HOME:
1840 case END:
1841 case TAB:
1842 case PAGEUP:
1843 case PAGEDOWN:
1844 case BACK:
1845 cancel();
1846 break;
1847 default:
1848 break;
1849 }
1850 }
1851 return false;
1852 }
1853
1854 /// cancel text typing on some mouse events, if necessary
1855 void onMouseEvent(MouseEvent event) {
1856 if (event.action == MouseAction.ButtonUp || event.action == MouseAction.ButtonDown)
1857 cancel();
1858 }
1859 }
1860
1861
1862 enum ONE_SECOND = 10_000_000L;
1863
1864 /// Helper to handle animation progress
1865 struct AnimationHelper {
1866 private long _timeElapsed;
1867 private long _maxInterval;
1868 private int _maxProgress;
1869
1870 /// start new animation interval
1871 void start(long maxInterval, int maxProgress) {
1872 _timeElapsed = 0;
1873 _maxInterval = maxInterval;
1874 _maxProgress = maxProgress;
1875 assert(_maxInterval > 0);
1876 assert(_maxProgress > 0);
1877 }
1878 /// Adds elapsed time; returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1879 int animate(long time) {
1880 _timeElapsed += time;
1881 return progress();
1882 }
1883 /// restart with same max interval and progress
1884 void restart() {
1885 if (!_maxInterval) {
1886 _maxInterval = ONE_SECOND;
1887 }
1888 _timeElapsed = 0;
1889 }
1890 /// returns time elapsed since start
1891 @property long elapsed() {
1892 return _timeElapsed;
1893 }
1894 /// get current time interval
1895 @property long interval() {
1896 return _maxInterval;
1897 }
1898 /// override current time interval, retaining the same progress %
1899 @property void interval(long newInterval) {
1900 int p = getProgress(10000);
1901 _maxInterval = newInterval;
1902 _timeElapsed = p * newInterval / 10000;
1903 }
1904 /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1905 @property int progress() {
1906 return getProgress(_maxProgress);
1907 }
1908 /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1909 int getProgress(int maxProgress) {
1910 if (finished)
1911 return maxProgress;
1912 if (_timeElapsed <= 0)
1913 return 0;
1914 return cast(int)(_timeElapsed * maxProgress / _maxInterval);
1915 }
1916 /// Returns true if animation is finished
1917 @property bool finished() {
1918 return _timeElapsed >= _maxInterval;
1919 }
1920 }
1921
1922
1923 /// mixin this to widget class to support tooltips based on widget's action label
1924 mixin template ActionTooltipSupport() {
1925 /// returns true if widget has tooltip to show
1926 override @property bool hasTooltip() {
1927 if (!_action || _action.labelValue.empty)
1928 return false;
1929 return true;
1930 }
1931 /// will be called from window once tooltip request timer expired; if null is returned, popup will not be shown; you can change alignment and position of popup here
1932 override Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) {
1933 Widget res = new TextWidget("tooltip", _action.tooltipText);
1934 res.styleId = STYLE_TOOLTIP;
1935 return res;
1936 }
1937 }
1938
1939 /// use in mixin to set this object property with name propName with value of variable value if variable name matches propName
1940 string generatePropertySetter(string propName) {
1941 return " if (name.equal(\"" ~ propName ~ "\")) { \n" ~
1942 " " ~ propName ~ " = value;\n" ~
1943 " return true;\n" ~
1944 " }\n";
1945 }
1946
1947 /// use in mixin to set this object properties with names from parameter list with value of variable value if variable name matches propName
1948 string generatePropertySetters(string[] propNames...) {
1949 string res;
1950 foreach(propName; propNames)
1951 res ~= generatePropertySetter(propName);
1952 return res;
1953 }
1954
1955 /// use in mixin for method override to set this object properties with names from parameter list with value of variable value if variable name matches propName
1956 string generatePropertySettersMethodOverride(string methodName, string typeName, string[] propNames...) {
1957 string res = " override bool " ~ methodName ~ "(string name, " ~ typeName ~ " value) {\n" ~
1958 " import std.algorithm : equal;\n";
1959 foreach(propName; propNames)
1960 res ~= generatePropertySetter(propName);
1961 res ~= " return super." ~ methodName ~ "(name, value);\n" ~
1962 " }\n";
1963 return res;
1964 }
1965
1966
1967 __gshared bool TOUCH_MODE = false;