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