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         if (oldHotkeys != newHotkeys)
502             requestLayout();
503         else
504             invalidate();
505         return this; 
506     }
507     /// returns font face
508     @property string fontFace() const { return stateStyle.fontFace; }
509     /// set font face for widget - override one from style
510     @property Widget fontFace(string face) { 
511         ownStyle.fontFace = face; 
512         requestLayout();
513         return this; 
514     }
515     /// returns font style (italic/normal)
516     @property bool fontItalic() const { return stateStyle.fontItalic; }
517     /// set font style (italic/normal) for widget - override one from style
518     @property Widget fontItalic(bool italic) { 
519         ownStyle.fontStyle = italic ? FONT_STYLE_ITALIC : FONT_STYLE_NORMAL; 
520         requestLayout();
521         return this; 
522     }
523     /// returns font weight
524     @property ushort fontWeight() const { return stateStyle.fontWeight; }
525     /// set font weight for widget - override one from style
526     @property Widget fontWeight(int weight) {
527         if (weight < 100)
528             weight = 100;
529         else if (weight > 900)
530             weight = 900;
531         ownStyle.fontWeight = cast(ushort)weight;
532         requestLayout();
533         return this; 
534     }
535     /// returns font size in pixels
536     @property int fontSize() const { return stateStyle.fontSize; }
537     /// set font size for widget - override one from style
538     @property Widget fontSize(int size) {
539         ownStyle.fontSize = size; 
540         requestLayout();
541         return this; 
542     }
543     /// returns font family
544     @property FontFamily fontFamily() const { return stateStyle.fontFamily; }
545     /// set font family for widget - override one from style
546     @property Widget fontFamily(FontFamily family) { 
547         ownStyle.fontFamily = family; 
548         requestLayout();
549         return this; 
550     }
551     /// returns alignment (combined vertical and horizontal)
552     @property ubyte alignment() const { return style.alignment; }
553     /// sets alignment (combined vertical and horizontal)
554     @property Widget alignment(ubyte value) { 
555         ownStyle.alignment = value; 
556         requestLayout();
557         return this; 
558     }
559     /// returns horizontal alignment
560     @property Align valign() { return cast(Align)(alignment & Align.VCenter); }
561     /// returns vertical alignment
562     @property Align halign() { return cast(Align)(alignment & Align.HCenter); }
563     /// returns font set for widget using style or set manually
564     @property FontRef font() const { return stateStyle.font; }
565 
566     /// returns widget content text (override to support this)
567     @property dstring text() { return ""; }
568     /// sets widget content text (override to support this)
569     @property Widget text(dstring s) { return this; }
570     /// sets widget content text (override to support this)
571     @property Widget text(UIString s) { return this; }
572 
573     //==================================================================
574     // Layout and drawing related methods
575 
576     /// returns true if layout is required for widget and its children
577     @property bool needLayout() { return _needLayout; }
578     /// returns true if redraw is required for widget and its children
579     @property bool needDraw() { return _needDraw; }
580     /// returns true is widget is being animated - need to call animate() and redraw
581     @property bool animating() { return false; }
582     /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second)
583     void animate(long interval) {
584     }
585     /// returns measured width (calculated during measure() call)
586     @property measuredWidth() { return _measuredWidth; }
587     /// returns measured height (calculated during measure() call)
588     @property measuredHeight() { return _measuredHeight; }
589     /// returns current width of widget in pixels
590     @property int width() { return _pos.width; }
591     /// returns current height of widget in pixels
592     @property int height() { return _pos.height; }
593     /// returns widget rectangle top position
594     @property int top() { return _pos.top; }
595     /// returns widget rectangle left position
596     @property int left() { return _pos.left; }
597     /// returns widget rectangle
598     @property Rect pos() { return _pos; }
599     /// returns min width constraint
600     @property int minWidth() { return style.minWidth; }
601     /// returns max width constraint (SIZE_UNSPECIFIED if no constraint set)
602     @property int maxWidth() { return style.maxWidth; }
603     /// returns min height constraint
604     @property int minHeight() { return style.minHeight; }
605     /// returns max height constraint (SIZE_UNSPECIFIED if no constraint set)
606     @property int maxHeight() { return style.maxHeight; }
607 
608     /// set max width constraint (SIZE_UNSPECIFIED for no constraint)
609     @property Widget maxWidth(int value) { ownStyle.maxWidth = value; return this; }
610     /// set max width constraint (0 for no constraint)
611     @property Widget minWidth(int value) { ownStyle.minWidth = value; return this; }
612     /// set max height constraint (SIZE_UNSPECIFIED for no constraint)
613     @property Widget maxHeight(int value) { ownStyle.maxHeight = value; return this; }
614     /// set max height constraint (0 for no constraint)
615     @property Widget minHeight(int value) { ownStyle.minHeight = value; return this; }
616 
617     /// returns layout width options (WRAP_CONTENT, FILL_PARENT, or some constant value)
618     @property int layoutWidth() { return style.layoutWidth; }
619     /// returns layout height options (WRAP_CONTENT, FILL_PARENT, or some constant value)
620     @property int layoutHeight() { return style.layoutHeight; }
621     /// returns layout weight (while resizing to fill parent, widget will be resized proportionally to this value)
622     @property int layoutWeight() { return style.layoutWeight; }
623 
624     /// sets layout width options (WRAP_CONTENT, FILL_PARENT, or some constant value)
625     @property Widget layoutWidth(int value) { ownStyle.layoutWidth = value; return this; }
626     /// sets layout height options (WRAP_CONTENT, FILL_PARENT, or some constant value)
627     @property Widget layoutHeight(int value) { ownStyle.layoutHeight = value; return this; }
628     /// sets layout weight (while resizing to fill parent, widget will be resized proportionally to this value)
629     @property Widget layoutWeight(int value) { ownStyle.layoutWeight = value; return this; }
630 
631     /// returns widget visibility (Visible, Invisible, Gone)
632     @property Visibility visibility() { return _visibility; }
633     /// sets widget visibility (Visible, Invisible, Gone)
634     @property Widget visibility(Visibility visible) {
635         if (_visibility != visible) {
636             if ((_visibility == Visibility.Gone) || (visible == Visibility.Gone)) {
637                 if (parent)
638                     parent.requestLayout();
639                 else
640                     requestLayout();
641             } else
642                 invalidate();
643             _visibility = visible;
644         }
645         return this;
646     }
647 
648     /// returns true if point is inside of this widget
649     bool isPointInside(int x, int y) {
650         return _pos.isPointInside(x, y);
651     }
652 
653     /// return true if state has State.Enabled flag set
654     @property bool enabled() { return (state & State.Enabled) != 0; }
655     /// change enabled state
656     @property Widget enabled(bool flg) { flg ? setState(State.Enabled) : resetState(State.Enabled); return this; }
657 
658     protected bool _clickable;
659     /// when true, user can click this control, and get onClick listeners called
660     @property bool clickable() { return _clickable; }
661     @property Widget clickable(bool flg) { _clickable = flg; return this; }
662     @property bool canClick() { return _clickable && enabled && visible; }
663 
664     protected bool _checkable;
665     /// when true, control supports Checked state
666     @property bool checkable() { return _checkable; }
667     @property Widget checkable(bool flg) { _checkable = flg; return this; }
668     @property bool canCheck() { return _checkable && enabled && visible; }
669 
670 
671     protected bool _checked;
672     /// get checked state
673     @property bool checked() { return (state & State.Checked) != 0; }
674     /// set checked state
675     @property Widget checked(bool flg) { 
676         if (flg != checked) {
677             if (flg) 
678                 setState(State.Checked); 
679             else 
680                 resetState(State.Checked); 
681             invalidate(); 
682         }
683         return this; 
684     }
685 
686     protected bool _focusable;
687     /// whether widget can be focused
688     @property bool focusable() const { return _focusable; }
689     @property Widget focusable(bool flg) { _focusable = flg; return this; }
690 
691     @property bool focused() const {
692         return (window !is null && window.focusedWidget is this && (state & State.Focused));
693     }
694 
695     /// override and return true to track key events even when not focused
696     @property bool wantsKeyTracking() {
697         return false;
698     }
699 
700     protected Action _action;
701     /// action to emit on click
702     @property const(Action) action() { return _action; }
703     /// action to emit on click
704     @property void action(const Action action) { _action = action.clone; handleActionStateChanged(); }
705     /// action to emit on click
706     @property void action(Action action) { _action = action; handleActionStateChanged(); }
707     /// ask for update state of some action (unles force=true, checks window flag actionsUpdateRequested), returns true if action state is changed
708     bool updateActionState(Action a, bool force = false, bool allowDefault = true) {
709         if (Window w = window) {
710             if (!force && !w.actionsUpdateRequested())
711                 return false;
712             const ActionState oldState = a.state;
713             //import dlangui.widgets.editors;
714             //if (a.id == EditorActions.Undo) {
715             //    Log.d("Requesting Undo action. Old state: ", a.state);
716             //}
717             if (w.dispatchActionStateRequest(a, this)) {
718                 // state is updated
719                 //Log.d("updateActionState ", a.label, " found state: ", a.state.toString);
720                 if (allowDefault)
721                     return true; // return 'request dispatched' flag instead of 'changed'
722             } else {
723                 if (!allowDefault)
724                     return false;
725                 a.state = a.defaultState;
726                 //Log.d("updateActionState ", a.label, " using default state: ", a.state.toString);
727             }
728             if (a.state != oldState)
729                 return true;
730         }
731         return false;
732     }
733     /// call to update state for action (if action is assigned for widget)
734     void updateActionState(bool force = false) {
735         if (!_action)
736             return;
737         if (updateActionState(_action, force))
738             handleActionStateChanged();
739     }
740     /// called when state of action assigned on widget is changed
741     void handleActionStateChanged() {
742         // override to update enabled state, visibility and checked state
743         // default processing: copy flags to this widget
744         updateStateFromAction(_action);
745     }
746     /// apply enabled, visibile and checked state for this widget from action's state
747     void updateStateFromAction(Action a) {
748         const ActionState s = a.state;
749         if (s.enabled != enabled) {
750             enabled = s.enabled;
751         }
752         if (s.checked != checked) {
753             checked = s.checked;
754         }
755         bool v = _visibility == Visibility.Visible;
756         if (s.visible != v) {
757             visibility = s.visible ? Visibility.Visible : Visibility.Gone;
758         }
759     }
760     /// set action update request flag, will be cleared after redraw
761     void requestActionsUpdate(bool immediateUpdate = false) {
762         if (Window w = window) {
763             w.requestActionsUpdate(immediateUpdate);
764         }
765     }
766 
767     protected UIString _tooltipText;
768     /// tooltip text - when not empty, widget will show tooltips automatically; for advanced tooltips - override hasTooltip and createTooltip
769     @property dstring tooltipText() { return _tooltipText; }
770     /// tooltip text - when not empty, widget will show tooltips automatically; for advanced tooltips - override hasTooltip and createTooltip
771     @property Widget tooltipText(dstring text) { _tooltipText = text; return this; }
772     /// tooltip text - when not empty, widget will show tooltips automatically; for advanced tooltips - override hasTooltip and createTooltip
773     @property Widget tooltipText(UIString text) { _tooltipText = text; return this; }
774 
775 
776     /// returns true if widget has tooltip to show
777     @property bool hasTooltip() {
778         return !_tooltipText.empty;
779     }
780     /// 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
781     Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) {
782         // default implementation supports tooltips when tooltipText property is set
783         if (!_tooltipText.empty) {
784             import dlangui.widgets.controls;
785             Widget res = new TextWidget("tooltip", _tooltipText.value);
786             res.styleId = STYLE_TOOLTIP;
787             return res;
788         }
789         return null;
790     }
791 
792     /// schedule tooltip
793     void scheduleTooltip(long delay = 300, uint alignment = 2 /*PopupAlign.Below*/, int x = 0, int y = 0) {
794         if (auto w = window)
795             w.scheduleTooltip(this, delay, alignment, x, y);
796     }
797 
798     protected bool _focusGroup;
799     /*****************************************
800      * 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.
801      * 
802      * If focused widget doesn't have any parent with focusGroup == true, focus may be moved to any focusable within window.
803      *
804      */
805     @property bool focusGroup() { return _focusGroup; }
806     /// set focus group flag for container widget
807     @property Widget focusGroup(bool flg) { _focusGroup = flg; return this; }
808     @property bool focusGroupFocused() const {
809         Widget w = focusGroupWidget();
810         return (w._state & State.WindowFocused) != 0;
811     }
812     protected bool setWindowFocusedFlag(bool flg) {
813         if (flg) {
814             if ((_state & State.WindowFocused) == 0) {
815                 _state |= State.WindowFocused;
816                 invalidate();
817                 return true;
818             }
819         } else {
820             if ((_state & State.WindowFocused) != 0) {
821                 _state &= ~State.WindowFocused;
822                 invalidate();
823                 return true;
824             }
825         }
826         return false;
827     }
828     @property Widget focusGroupFocused(bool flg) {
829         Widget w = focusGroupWidget();
830         w.setWindowFocusedFlag(flg);
831         while (w.parent) {
832             w = w.parent;
833             if (w.parent is null || w.focusGroup) {
834                 w.setWindowFocusedFlag(flg);
835             }
836         }
837         return this;
838     }
839 
840     /// find nearest parent of this widget with focusGroup flag, returns topmost parent if no focusGroup flag set to any of parents.
841     Widget focusGroupWidget() inout {
842         Widget p = cast(Widget)this;
843         while (p) {
844             if (!p.parent || p.focusGroup)
845                 break;
846             p = p.parent;
847         }
848         return p;
849     }
850 
851     private static class TabOrderInfo {
852         Widget widget;
853         uint tabOrder;
854         uint childOrder;
855         Rect rect;
856         this(Widget widget, Rect rect) {
857             this.widget = widget;
858             this.tabOrder = widget.thisOrParentTabOrder();
859             this.rect = widget.pos;
860         }
861         static if (BACKEND_GUI) {
862             static immutable int NEAR_THRESHOLD = 10;
863         } else {
864             static immutable int NEAR_THRESHOLD = 1;
865         }
866         bool nearX(TabOrderInfo v) {
867             return v.rect.left >= rect.left - NEAR_THRESHOLD  && v.rect.left <= rect.left + NEAR_THRESHOLD;
868         }
869         bool nearY(TabOrderInfo v) {
870             return v.rect.top >= rect.top - NEAR_THRESHOLD  && v.rect.top <= rect.top + NEAR_THRESHOLD;
871         }
872         override int opCmp(Object obj) const {
873             TabOrderInfo v = cast(TabOrderInfo)obj;
874             if (tabOrder != 0 && v.tabOrder !=0) {
875                 if (tabOrder < v.tabOrder)
876                     return -1;
877                 if (tabOrder > v.tabOrder)
878                     return 1;
879             }
880             // place items with tabOrder 0 after items with tabOrder non-0
881             if (tabOrder != 0)
882                 return -1;
883             if (v.tabOrder != 0)
884                 return 1;
885             if (childOrder < v.childOrder)
886                 return -1;
887             if (childOrder > v.childOrder)
888                 return 1;
889             return 0;
890         }
891         /// less predicat for Left/Right sorting
892         static bool lessHorizontal(TabOrderInfo obj1, TabOrderInfo obj2) {
893             if (obj1.nearY(obj2)) {
894                 return obj1.rect.left < obj2.rect.left;
895             }
896             return obj1.rect.top < obj2.rect.top;
897         }
898         /// less predicat for Up/Down sorting
899         static bool lessVertical(TabOrderInfo obj1, TabOrderInfo obj2) {
900             if (obj1.nearX(obj2)) {
901                 return obj1.rect.top < obj2.rect.top;
902             }
903             return obj1.rect.left < obj2.rect.left;
904         }
905         override string toString() const {
906             return widget.id;
907         }
908     }
909 
910     private void findFocusableChildren(ref TabOrderInfo[] results, Rect clipRect, Widget currentWidget) {
911         if (visibility != Visibility.Visible)
912             return;
913         Rect rc = _pos;
914         applyMargins(rc);
915         applyPadding(rc);
916         if (!rc.intersects(clipRect))
917             return; // out of clip rectangle
918         if (canFocus || this is currentWidget) {
919             TabOrderInfo item = new TabOrderInfo(this, rc);
920             results ~= item;
921             return;
922         }
923         rc.intersect(clipRect);
924         for (int i = 0; i < childCount(); i++) {
925             child(i).findFocusableChildren(results, rc, currentWidget);
926         }
927     }
928 
929     /// find all focusables belonging to the same focusGroup as this widget (does not include current widget).
930     /// usually to be called for focused widget to get possible alternatives to navigate to
931     private TabOrderInfo[] findFocusables(Widget currentWidget) {
932         TabOrderInfo[] result;
933         Widget group = focusGroupWidget();
934         group.findFocusableChildren(result, group.pos, currentWidget);
935         for (ushort i = 0; i < result.length; i++)
936             result[i].childOrder = i + 1;
937         sort(result);
938         return result;
939     }
940 
941     protected ushort _tabOrder;
942     /// tab order - hint for focus movement using Tab/Shift+Tab
943     @property ushort tabOrder() { return _tabOrder; }
944     @property Widget tabOrder(ushort tabOrder) { _tabOrder = tabOrder; return this; }
945     private int thisOrParentTabOrder() {
946         if (_tabOrder)
947             return _tabOrder;
948         if (!parent)
949             return 0;
950         return parent.thisOrParentTabOrder;
951     }
952 
953     /// call on focused widget, to find best 
954     private Widget findNextFocusWidget(FocusMovement direction) {
955         if (direction == FocusMovement.None)
956             return this;
957         TabOrderInfo[] focusables = findFocusables(this);
958         if (!focusables.length)
959             return null;
960         int myIndex = -1;
961         for (int i = 0; i < focusables.length; i++) {
962             if (focusables[i].widget is this) {
963                 myIndex = i;
964                 break;
965             }
966         }
967         debug(DebugFocus) Log.d("findNextFocusWidget myIndex=", myIndex, " of focusables: ", focusables);
968         if (myIndex == -1)
969             return null; // not found myself
970         if (focusables.length == 1)
971             return focusables[0].widget; // single option - use it
972         if (direction == FocusMovement.Next) {
973             // move forward
974             int index = myIndex + 1;
975             if (index >= focusables.length)
976                 index = 0;
977             return focusables[index].widget;
978         } else if (direction == FocusMovement.Previous) {
979             // move back
980             int index = myIndex - 1;
981             if (index < 0)
982                 index = cast(int)focusables.length - 1;
983             return focusables[index].widget;
984         } else {
985             // Left, Right, Up, Down
986             if (direction == FocusMovement.Left || direction == FocusMovement.Right) {
987                 sort!(TabOrderInfo.lessHorizontal)(focusables);
988             } else {
989                 sort!(TabOrderInfo.lessVertical)(focusables);
990             }
991             myIndex = 0;
992             for (int i = 0; i < focusables.length; i++) {
993                 if (focusables[i].widget is this) {
994                     myIndex = i;
995                     break;
996                 }
997             }
998             int index = myIndex;
999             if (direction == FocusMovement.Left || direction == FocusMovement.Up) {
1000                 index--;
1001                 if (index < 0)
1002                     index = cast(int)focusables.length - 1;
1003             } else {
1004                 index++;
1005                 if (index >= focusables.length)
1006                     index = 0;
1007             }
1008             return focusables[index].widget;
1009         }
1010     }
1011 
1012     bool handleMoveFocusUsingKeys(KeyEvent event) {
1013         if (!focused || !visible)
1014             return false;
1015         if (event.action != KeyAction.KeyDown)
1016             return false;
1017         FocusMovement direction = FocusMovement.None;
1018         uint flags = event.flags & (KeyFlag.Shift | KeyFlag.Control | KeyFlag.Alt);
1019         switch (event.keyCode) with(KeyCode)
1020         {
1021             case LEFT:
1022                 if (flags == 0)
1023                     direction = FocusMovement.Left;
1024                 break;
1025             case RIGHT:
1026                 if (flags == 0)
1027                     direction = FocusMovement.Right;
1028                 break;
1029             case UP:
1030                 if (flags == 0)
1031                     direction = FocusMovement.Up;
1032                 break;
1033             case DOWN:
1034                 if (flags == 0)
1035                     direction = FocusMovement.Down;
1036                 break;
1037             case TAB:
1038                 if (flags == 0)
1039                     direction = FocusMovement.Next;
1040                 else if (flags == KeyFlag.Shift)
1041                     direction = FocusMovement.Previous;
1042                 break;
1043             default:
1044                 break;
1045         }
1046         if (direction == FocusMovement.None)
1047             return false;
1048         Widget nextWidget = findNextFocusWidget(direction);
1049         if (!nextWidget)
1050             return false;
1051         nextWidget.setFocus(FocusReason.TabFocus);
1052         return true;
1053     }
1054 
1055     /// returns true if this widget and all its parents are visible
1056     @property bool visible() {
1057         if (visibility != Visibility.Visible)
1058             return false;
1059         if (parent is null)
1060             return true;
1061         return parent.visible;
1062     }
1063 
1064     /// returns true if widget is focusable and visible and enabled
1065     @property bool canFocus() {
1066         return focusable && visible && enabled;
1067     }
1068 
1069     /// sets focus to this widget or suitable focusable child, returns previously focused widget
1070     Widget setFocus(FocusReason reason = FocusReason.Unspecified) {
1071         if (window is null)
1072             return null;
1073         if (!visible)
1074             return window.focusedWidget;
1075         invalidate();
1076         if (!canFocus) {
1077             Widget w = findFocusableChild(true);
1078             if (!w)
1079                 w = findFocusableChild(false);
1080             if (w)
1081                 return window.setFocus(w, reason);
1082             // try to find focusable child
1083             return window.focusedWidget;
1084         }
1085         return window.setFocus(this, reason);
1086     }
1087     /// searches children for first focusable item, returns null if not found
1088     Widget findFocusableChild(bool defaultOnly) {
1089         for(int i = 0; i < childCount; i++) {
1090             Widget w = child(i);
1091             if (w.canFocus && (!defaultOnly || (w.state & State.Default) != 0))
1092                 return w;
1093             w = w.findFocusableChild(defaultOnly);
1094             if (w !is null)
1095                 return w;
1096         }
1097         if (canFocus)
1098             return this;
1099         return null;
1100     }
1101 
1102     // =======================================================
1103     // Events
1104 
1105     protected ActionMap _acceleratorMap;
1106     @property ref ActionMap acceleratorMap() { return _acceleratorMap; }
1107 
1108     /// override to handle specific actions
1109     bool handleAction(const Action a) {
1110         if (onAction.assigned)
1111             if (onAction(this, a))
1112                 return true;
1113         return false;
1114     }
1115     /// override to handle specific actions state (e.g. change enabled state for supported actions)
1116     bool handleActionStateRequest(const Action a) {
1117         return false;
1118     }
1119 
1120     /// call to dispatch action
1121     bool dispatchAction(const Action a) {
1122         if (window)
1123             return window.dispatchAction(a, this);
1124         else
1125             return handleAction(a);
1126     }
1127 
1128     // called to process click and notify listeners
1129     protected bool handleClick() {
1130         bool res = false;
1131         if (click.assigned)
1132             res = click(this);
1133         else if (_action) {
1134             return dispatchAction(_action);
1135         }
1136         return res;
1137     }
1138 
1139 
1140     void cancelLayout() {
1141         _needLayout = false;
1142     }
1143 
1144     /// set new timer to call onTimer() after specified interval (for recurred notifications, return true from onTimer)
1145     ulong setTimer(long intervalMillis) {
1146         if (auto w = window)
1147             return w.setTimer(this, intervalMillis);
1148         return 0; // no window - no timer
1149     }
1150 
1151     /// cancel timer - pass value returned from setTimer() as timerId parameter
1152     void cancelTimer(ulong timerId) {
1153         if (auto w = window)
1154             w.cancelTimer(timerId);
1155     }
1156 
1157     /// handle timer; return true to repeat timer event after next interval, false cancel timer
1158     bool onTimer(ulong id) {
1159         // override to do something useful
1160         // return true to repeat after the same interval, false to stop timer
1161         return false;
1162     }
1163 
1164     /// map key to action
1165     Action findKeyAction(uint keyCode, uint flags) {
1166         Action action = _acceleratorMap.findByKey(keyCode, flags);
1167         if (action)
1168             return action;
1169         if (keyToAction.assigned)
1170             action = keyToAction(this, keyCode, flags);
1171         return action;
1172     }
1173 
1174     /// process key event, return true if event is processed.
1175     bool onKeyEvent(KeyEvent event) {
1176         if (keyEvent.assigned && keyEvent(this, event))
1177             return true; // processed by external handler
1178         if (event.action == KeyAction.KeyDown) {
1179             //Log.d("Find key action for key = ", event.keyCode, " flags=", event.flags);
1180             Action action = findKeyAction(event.keyCode, event.flags); // & (KeyFlag.Shift | KeyFlag.Alt | KeyFlag.Control | KeyFlag.Menu)
1181             if (action !is null) {
1182                 //Log.d("Action found: ", action.id, " ", action.labelValue.id);
1183                 return dispatchAction(action);
1184             }
1185         }
1186         // handle focus navigation using keys
1187         if (focused && handleMoveFocusUsingKeys(event))
1188             return true;
1189         if (canClick) {
1190             // support onClick event initiated by Space or Return keys
1191             if (event.action == KeyAction.KeyDown) {
1192                 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) {
1193                     setState(State.Pressed);
1194                     return true;
1195                 }
1196             }
1197             if (event.action == KeyAction.KeyUp) {
1198                 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) {
1199                     resetState(State.Pressed);
1200                     handleClick();
1201                     return true;
1202                 }
1203             }
1204         }
1205         return false;
1206     }
1207 
1208     /// handle custom event
1209     bool onEvent(CustomEvent event) {
1210         RunnableEvent runnable = cast(RunnableEvent)event;
1211         if (runnable) {
1212             // handle runnable
1213             runnable.run();
1214             return true;
1215         }
1216         // override to handle more events
1217         return false;
1218     }
1219 
1220     /// 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)
1221     void executeInUiThread(void delegate() runnable) {
1222         if (!window)
1223             return;
1224         RunnableEvent event = new RunnableEvent(CUSTOM_RUNNABLE, this, runnable);
1225         window.postEvent(event);
1226     }
1227 
1228     /// process mouse event; return true if event is processed by widget.
1229     bool onMouseEvent(MouseEvent event) {
1230         if (mouseEvent.assigned && mouseEvent(this, event))
1231             return true; // processed by external handler
1232         //Log.d("onMouseEvent ", id, " ", event.action, "  (", event.x, ",", event.y, ")");
1233         // support onClick
1234         if (canClick) {
1235             if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
1236                 setState(State.Pressed);
1237                 if (canFocus)
1238                     setFocus();
1239                 return true;
1240             }
1241             if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
1242                 resetState(State.Pressed);
1243                 handleClick();
1244                 return true;
1245             }
1246             if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
1247                 resetState(State.Pressed);
1248                 resetState(State.Hovered);
1249                 return true;
1250             }
1251             if (event.action == MouseAction.FocusIn) {
1252                 setState(State.Pressed);
1253                 return true;
1254             }
1255         }
1256         if (event.action == MouseAction.Move && !event.hasModifiers && hasTooltip) {
1257             scheduleTooltip(200);
1258         }
1259         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Right) {
1260             if (canShowPopupMenu(event.x, event.y)) {
1261                 showPopupMenu(event.x, event.y);
1262                 return true;
1263             }
1264         }
1265         if (canFocus && event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
1266             setFocus();
1267             return true;
1268         }
1269         if (trackHover) {
1270             if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
1271                 if ((state & State.Hovered)) {
1272                     debug(mouse) Log.d("Hover off ", id);
1273                     resetState(State.Hovered);
1274                 }
1275                 return true;
1276             }
1277             if (event.action == MouseAction.Move) {
1278                 if (!(state & State.Hovered)) {
1279                     debug(mouse) Log.d("Hover ", id);
1280                     if (!TOUCH_MODE)
1281                         setState(State.Hovered);
1282                 }
1283                 return true;
1284             }
1285             if (event.action == MouseAction.Leave) {
1286                 debug(mouse) Log.d("Leave ", id);
1287                 resetState(State.Hovered);
1288                 return true;
1289             }
1290         }
1291         return false;
1292     }
1293 
1294     // =======================================================
1295     // Signals
1296 
1297     /// on click event listener (bool delegate(Widget))
1298     Signal!OnClickHandler click;
1299 
1300     /// checked state change event listener (bool delegate(Widget, bool))
1301     Signal!OnCheckHandler checkChange;
1302 
1303     /// focus state change event listener (bool delegate(Widget, bool))
1304     Signal!OnFocusHandler focusChange;
1305 
1306     /// key event listener (bool delegate(Widget, KeyEvent)) - return true if event is processed by handler
1307     Signal!OnKeyHandler keyEvent;
1308 
1309     /// action by key lookup handler
1310     Listener!OnKeyActionHandler keyToAction;
1311 
1312     /// action handlers
1313     Signal!OnActionHandler onAction;
1314 
1315     /// mouse event listener (bool delegate(Widget, MouseEvent)) - return true if event is processed by handler
1316     Signal!OnMouseHandler mouseEvent;
1317 
1318 
1319     // Signal utils
1320 
1321     /// helper function to add onCheckChangeListener in method chain
1322     Widget addOnClickListener(bool delegate(Widget) listener) {
1323         click.connect(listener);
1324         return this;
1325     }
1326 
1327     /// helper function to add onCheckChangeListener in method chain
1328     Widget addOnCheckChangeListener(bool delegate(Widget, bool) listener) {
1329         checkChange.connect(listener);
1330         return this;
1331     }
1332 
1333     /// helper function to add onFocusChangeListener in method chain
1334     Widget addOnFocusChangeListener(bool delegate(Widget, bool) listener) {
1335         focusChange.connect(listener);
1336         return this;
1337     }
1338 
1339     // =======================================================
1340     // Layout and measurement methods
1341 
1342     /// request relayout of widget and its children
1343     void requestLayout() {
1344         _needLayout = true;
1345     }
1346     /// request redraw
1347     void invalidate() {
1348         _needDraw = true;
1349     }
1350 
1351     /// helper function for implement measure() when widget's content dimensions are known
1352     protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) {
1353         if (visibility == Visibility.Gone) {
1354             _measuredWidth = _measuredHeight = 0;
1355             return;
1356         }
1357         Rect m = margins;
1358         Rect p = padding;
1359         // summarize margins, padding, and content size
1360         int dx = m.left + m.right + p.left + p.right + contentWidth;
1361         int dy = m.top + m.bottom + p.top + p.bottom + contentHeight;
1362         // check for fixed size set in layoutWidth, layoutHeight
1363         int lh = layoutHeight;
1364         int lw = layoutWidth;
1365         if (isPercentSize(lh) && parentHeight != SIZE_UNSPECIFIED)
1366             dy = fromPercentSize(lh, parentHeight);
1367         else if (!isSpecialSize(lh))
1368             dy = lh;
1369         if (isPercentSize(lw) && parentWidth != SIZE_UNSPECIFIED)
1370             dx = fromPercentSize(lw, parentWidth);
1371         else if (!isSpecialSize(lw))
1372             dx = lw;
1373         // apply min/max width and height constraints
1374         int minw = minWidth;
1375         int maxw = maxWidth;
1376         int minh = minHeight;
1377         int maxh = maxHeight;
1378         if (minw != SIZE_UNSPECIFIED && dx < minw)
1379             dx = minw;
1380         if (minh != SIZE_UNSPECIFIED && dy < minh)
1381             dy = minh;
1382         if (maxw != SIZE_UNSPECIFIED && dx > maxw)
1383             dx = maxw;
1384         if (maxh != SIZE_UNSPECIFIED && dy > maxh)
1385             dy = maxh;
1386         // apply FILL_PARENT
1387         //if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT)
1388         //    dx = parentWidth;
1389         //if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT)
1390         //    dy = parentHeight;
1391         // apply max parent size constraint
1392         if (parentWidth != SIZE_UNSPECIFIED && dx > parentWidth)
1393             dx = parentWidth;
1394         if (parentHeight != SIZE_UNSPECIFIED && dy > parentHeight)
1395             dy = parentHeight;
1396         _measuredWidth = dx;
1397         _measuredHeight = dy;
1398     }
1399 
1400     /** 
1401         Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 
1402 
1403     */
1404     void measure(int parentWidth, int parentHeight) { 
1405         measuredContent(parentWidth, parentHeight, 0, 0);
1406     }
1407 
1408     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
1409     void layout(Rect rc) {
1410         if (visibility == Visibility.Gone) {
1411             return;
1412         }
1413         _pos = rc;
1414         _needLayout = false;
1415     }
1416 
1417     /// draws focus rectangle, if enabled in styles
1418     void drawFocusRect(DrawBuf buf, Rect rc) {
1419         const uint[] colors = focusRectColors;
1420         if (colors) {
1421             buf.drawFocusRect(rc, colors);
1422         }
1423     }
1424 
1425     /// Draw widget at its position to buffer
1426     void onDraw(DrawBuf buf) {
1427         if (visibility != Visibility.Visible)
1428             return;
1429         Rect rc = _pos;
1430         applyMargins(rc);
1431         auto saver = ClipRectSaver(buf, rc, alpha);
1432         DrawableRef bg = backgroundDrawable;
1433         if (!bg.isNull) {
1434             bg.drawTo(buf, rc, state);
1435         }
1436         applyPadding(rc);
1437         if (state & State.Focused) {
1438             rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
1439             drawFocusRect(buf, rc);
1440         }
1441         _needDraw = false;
1442     }
1443 
1444     /// Helper function: applies margins to rectangle
1445     void applyMargins(ref Rect rc) {
1446         Rect m = margins;
1447         rc.left += m.left;
1448         rc.top += m.top;
1449         rc.bottom -= m.bottom;
1450         rc.right -= m.right;
1451     }
1452     /// Helper function: applies padding to rectangle
1453     void applyPadding(ref Rect rc) {
1454         Rect m = padding;
1455         rc.left += m.left;
1456         rc.top += m.top;
1457         rc.bottom -= m.bottom;
1458         rc.right -= m.right;
1459     }
1460     /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
1461     static void applyAlign(ref Rect rc, Point sz, Align ha, Align va) {
1462         if (va == Align.Bottom) {
1463             rc.top = rc.bottom - sz.y;
1464         } else if (va == Align.VCenter) {
1465             int dy = (rc.height - sz.y) / 2;
1466             rc.top += dy;
1467             rc.bottom = rc.top + sz.y;
1468         } else {
1469             rc.bottom = rc.top + sz.y;
1470         }
1471         if (ha == Align.Right) {
1472             rc.left = rc.right - sz.x;
1473         } else if (ha == Align.HCenter) {
1474             int dx = (rc.width - sz.x) / 2;
1475             rc.left += dx;
1476             rc.right = rc.left + sz.x;
1477         } else {
1478             rc.right = rc.left + sz.x;
1479         }
1480     }
1481     /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
1482     void applyAlign(ref Rect rc, Point sz) {
1483         Align va = valign;
1484         Align ha = halign;
1485         applyAlign(rc, sz, ha, va);
1486     }
1487 
1488     // ===========================================================
1489     // popup menu support
1490     /// returns true if widget can show popup menu (e.g. by mouse right click at point x,y)
1491     bool canShowPopupMenu(int x, int y) {
1492         return false;
1493     }
1494     /// shows popup menu at (x,y)
1495     void showPopupMenu(int x, int y) {
1496         // override to show popup
1497     }
1498     /// override to change popup menu items state
1499     bool isActionEnabled(const Action action) {
1500         return true;
1501     }
1502 
1503     // ===========================================================
1504     // Widget hierarhy methods
1505 
1506     /// returns number of children of this widget
1507     @property int childCount() { return 0; }
1508     /// returns child by index
1509     Widget child(int index) { return null; }
1510     /// adds child, returns added item
1511     Widget addChild(Widget item) { assert(false, "addChild: children not suported for this widget type"); }
1512     /// adds child, returns added item
1513     Widget addChildren(Widget[] items) { 
1514         foreach(item; items) {
1515             addChild(item); 
1516         }
1517         return this;
1518     }
1519     /// inserts child at given index, returns inserted item
1520     Widget insertChild(Widget item, int index) {assert(false, "insertChild: children not suported for this widget type"); }
1521     /// removes child, returns removed item
1522     Widget removeChild(int index) { assert(false, "removeChild: children not suported for this widget type"); }
1523     /// removes child by ID, returns removed item
1524     Widget removeChild(string id) { assert(false, "removeChild: children not suported for this widget type"); }
1525     /// removes child, returns removed item
1526     Widget removeChild(Widget child) { assert(false, "removeChild: children not suported for this widget type"); }
1527     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
1528     int childIndex(Widget item) { return -1; }
1529 
1530 
1531     /// 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).
1532     bool isChild(Widget item, bool deepSearch = true) {
1533         if (deepSearch) {
1534             // this widget or some widget inside children tree
1535             if (item is this)
1536                 return true;
1537             for (int i = 0; i < childCount; i++) {
1538                 if (child(i).isChild(item))
1539                     return true;
1540             }
1541         } else {
1542             // only one of children
1543             for (int i = 0; i < childCount; i++) {
1544                 if (item is child(i))
1545                     return true;
1546             }
1547         }
1548         return false;
1549     }
1550 
1551     /// find child of specified type T by id, returns null if not found or cannot be converted to type T
1552     T childById(T = typeof(this))(string id, bool deepSearch = true) { 
1553         if (deepSearch) {
1554             // search everywhere inside child tree
1555             if (compareId(id)) {
1556                 T found = cast(T)this;
1557                 if (found)
1558                     return found;
1559             }
1560             // lookup children
1561             for (int i = childCount - 1; i >= 0; i--) {
1562                 Widget res = child(i).childById(id);
1563                 if (res !is null) {
1564                     T found = cast(T)res;
1565                     if (found)
1566                         return found;
1567                 }
1568             }
1569         } else {
1570             // search only across children of this widget
1571             for (int i = childCount - 1; i >= 0; i--) {
1572                 Widget w = child(i);
1573                 if (id.equal(w.id)) {
1574                     T found = cast(T)w;
1575                     if (found)
1576                         return found;
1577                 }
1578             }
1579         }
1580         // not found
1581         return null; 
1582     }
1583 
1584     /// returns parent widget, null for top level widget
1585     @property Widget parent() const { return _parent ? cast(Widget)_parent : null; }
1586     /// sets parent for widget
1587     @property Widget parent(Widget parent) { _parent = parent; return this; }
1588     /// returns window (if widget or its parent is attached to window)
1589     @property Window window() const {
1590         Widget p = cast(Widget)this;
1591         while (p !is null) {
1592             if (p._window !is null)
1593                 return cast(Window)p._window;
1594             p = p.parent;
1595         }
1596         return null;
1597     }
1598     /// sets window (to be used for top level widget from Window implementation). TODO: hide it from API?
1599     @property void window(Window window) { 
1600         _window = window; 
1601     }
1602 
1603     void removeAllChildren(bool destroyObj = true) {
1604         // override
1605     }
1606 
1607     /// set string property value, for ML loaders
1608     bool setStringProperty(string name, string value) {
1609         mixin(generatePropertySetters("id", "styleId", "backgroundImageId", "backgroundColor", "textColor", "fontFace"));
1610         if (name.equal("text")) {
1611             text = UIString.fromId(value);
1612             return true;
1613         }
1614         if (name.equal("tooltipText")) {
1615             tooltipText = UIString.fromId(value);
1616             return true;
1617         }
1618         return false;
1619     }
1620 
1621     /// set string property value, for ML loaders
1622     bool setDstringProperty(string name, dstring value) {
1623         if (name.equal("text")) {
1624             text = UIString.fromRaw(value);
1625             return true;
1626         }
1627         if (name.equal("tooltipText")) {
1628             tooltipText = UIString.fromRaw(value);
1629             return true;
1630         }
1631         return false;
1632     }
1633 
1634     /// set string property value, for ML loaders
1635     bool setUistringProperty(string name, UIString value) {
1636         if (name.equal("text")) {
1637             text = value;
1638             return true;
1639         }
1640         if (name.equal("tooltipText")) {
1641             tooltipText = value;
1642             return true;
1643         }
1644         return false;
1645     }
1646 
1647     /// StringListValue list values
1648     bool setStringListValueListProperty(string propName, StringListValue[] values) {
1649         return false;
1650     }
1651 
1652     /// UIString list values
1653     bool setUIStringListProperty(string propName, UIString[] values) {
1654         return false;
1655     }
1656 
1657     /// set string property value, for ML loaders
1658     bool setBoolProperty(string name, bool value) {
1659         mixin(generatePropertySetters("enabled", "clickable", "checkable", "focusable", "checked", "fontItalic"));
1660         return false;
1661     }
1662 
1663     /// set double property value, for ML loaders
1664     bool setDoubleProperty(string name, double value) {
1665         if (name.equal("alpha")) {
1666             int n = cast(int)(value * 255);
1667             return setIntProperty(name, n);
1668         }
1669         return false;
1670     }
1671 
1672     /// set int property value, for ML loaders
1673     bool setIntProperty(string name, int value) {
1674         if (name.equal("alpha")) {
1675             if (value < 0)
1676                 value = 0;
1677             else if (value > 255)
1678                 value = 255;
1679             alpha = cast(ushort)value;
1680             return true;
1681         }
1682         mixin(generatePropertySetters("minWidth", "maxWidth", "minHeight", "maxHeight", "layoutWidth", "layoutHeight", "layoutWeight", "textColor", "backgroundColor", "fontSize", "fontWeight"));
1683         if (name.equal("margins")) { // use same value for all sides
1684             margins = Rect(value, value, value, value);
1685             return true;
1686         }
1687         if (name.equal("alignment")) {
1688             alignment = cast(Align)value;
1689             return true;
1690         }
1691         if (name.equal("padding")) { // use same value for all sides
1692             padding = Rect(value, value, value, value);
1693             return true;
1694         }
1695         return false;
1696     }
1697 
1698     /// set Rect property value, for ML loaders
1699     bool setRectProperty(string name, Rect value) {
1700         mixin(generatePropertySetters("margins", "padding"));
1701         return false;
1702     }
1703 }
1704 
1705 /** Widget list holder. */
1706 alias WidgetList = ObjectList!Widget;
1707 
1708 /** Base class for widgets which have children. */
1709 class WidgetGroup : Widget {
1710 
1711     /// empty parameter list constructor - for usage by factory
1712     this() {
1713         this(null);
1714     }
1715     /// create with ID parameter
1716     this(string ID) {
1717         super(ID);
1718     }
1719 
1720     protected WidgetList _children;
1721 
1722     /// returns number of children of this widget
1723     @property override int childCount() { return _children.count; }
1724     /// returns child by index
1725     override Widget child(int index) { return _children.get(index); }
1726     /// adds child, returns added item
1727     override Widget addChild(Widget item) { return _children.add(item).parent(this); }
1728     /// inserts child at given index, returns inserted item
1729     override Widget insertChild(Widget item, int index) { return _children.insert(item,index).parent(this); }
1730     /// removes child, returns removed item
1731     override Widget removeChild(int index) { 
1732         Widget res = _children.remove(index);
1733         if (res !is null)
1734             res.parent = null;
1735         return res;
1736     }
1737     /// removes child by ID, returns removed item
1738     override Widget removeChild(string ID) {
1739         Widget res = null;
1740         int index = _children.indexOf(ID);
1741         if (index < 0)
1742             return null;
1743         res = _children.remove(index); 
1744         if (res !is null)
1745             res.parent = null;
1746         return res;
1747     }
1748     /// removes child, returns removed item
1749     override Widget removeChild(Widget child) {
1750         Widget res = null;
1751         int index = _children.indexOf(child);
1752         if (index < 0)
1753             return null;
1754         res = _children.remove(index); 
1755         if (res !is null)
1756             res.parent = null;
1757         return res;
1758     }
1759     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
1760     override int childIndex(Widget item) { return _children.indexOf(item); }
1761 
1762 	override void removeAllChildren(bool destroyObj = true) {
1763         _children.clear(destroyObj);
1764     }
1765 
1766     /// replace child with other child
1767     void replaceChild(Widget newChild, Widget oldChild) {
1768         _children.replace(newChild, oldChild);
1769     }
1770 
1771 }
1772 
1773 /** WidgetGroup with default drawing of children (just draw all children) */
1774 class WidgetGroupDefaultDrawing : WidgetGroup {
1775     /// empty parameter list constructor - for usage by factory
1776     this() {
1777         this(null);
1778     }
1779     /// create with ID parameter
1780     this(string ID) {
1781         super(ID);
1782     }
1783     /// Draw widget at its position to buffer
1784     override void onDraw(DrawBuf buf) {
1785         if (visibility != Visibility.Visible)
1786             return;
1787         super.onDraw(buf);
1788         Rect rc = _pos;
1789         applyMargins(rc);
1790         applyPadding(rc);
1791         auto saver = ClipRectSaver(buf, rc);
1792         for (int i = 0; i < _children.count; i++) {
1793             Widget item = _children.get(i);
1794             item.onDraw(buf);
1795         }
1796     }
1797 }
1798 
1799 /// helper for locating items in list, tree, table or other controls by typing their name
1800 struct TextTypingShortcutHelper {
1801     int timeoutMillis = 800; // expiration time for entered text; after timeout collected text will be cleared
1802     private long _lastUpdateTimeStamp;
1803     private dchar[] _text;
1804     /// cancel text collection (next typed text will be collected from scratch)
1805     void cancel() {
1806         _text.length = 0;
1807         _lastUpdateTimeStamp = 0;
1808     }
1809     /// returns collected text string - use it for lookup
1810     @property dstring text() { return _text.dup; }
1811     /// pass key event here; returns true if search text is updated and you can move selection using it
1812     bool onKeyEvent(KeyEvent event) {
1813         long ts = currentTimeMillis;
1814         if (_lastUpdateTimeStamp && ts - _lastUpdateTimeStamp > timeoutMillis)
1815             cancel();
1816         if (event.action == KeyAction.Text) {
1817             _text ~= event.text;
1818             _lastUpdateTimeStamp = ts;
1819             return _text.length > 0;
1820         }
1821         if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
1822             switch (event.keyCode) with (KeyCode) {
1823                 case LEFT:
1824                 case RIGHT:
1825                 case UP:
1826                 case DOWN:
1827                 case HOME:
1828                 case END:
1829                 case TAB:
1830                 case PAGEUP:
1831                 case PAGEDOWN:
1832                 case BACK:
1833                     cancel();
1834                     break;
1835                 default:
1836                     break;
1837             }
1838         }
1839         return false;
1840     }
1841 
1842     /// cancel text typing on some mouse events, if necessary
1843     void onMouseEvent(MouseEvent event) {
1844         if (event.action == MouseAction.ButtonUp || event.action == MouseAction.ButtonDown)
1845             cancel();
1846     }
1847 }
1848 
1849 
1850 enum ONE_SECOND = 10_000_000L;
1851 
1852 /// Helper to handle animation progress
1853 struct AnimationHelper {
1854     private long _timeElapsed;
1855     private long _maxInterval;
1856     private int  _maxProgress;
1857 
1858     /// start new animation interval
1859     void start(long maxInterval, int maxProgress) {
1860         _timeElapsed = 0;
1861         _maxInterval = maxInterval;
1862         _maxProgress = maxProgress;
1863         assert(_maxInterval > 0);
1864         assert(_maxProgress > 0);
1865     }
1866     /// Adds elapsed time; returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1867     int animate(long time) {
1868         _timeElapsed += time;
1869         return progress();
1870     }
1871     /// restart with same max interval and progress
1872     void restart() {
1873         if (!_maxInterval) {
1874             _maxInterval = ONE_SECOND;
1875         }
1876         _timeElapsed = 0;
1877     }
1878     /// returns time elapsed since start
1879     @property long elapsed() {
1880         return _timeElapsed;
1881     }
1882     /// get current time interval
1883     @property long interval() {
1884         return _maxInterval;
1885     }
1886     /// override current time interval, retaining the same progress %
1887     @property void interval(long newInterval) {
1888         int p = getProgress(10000);
1889         _maxInterval = newInterval;
1890         _timeElapsed = p * newInterval / 10000;
1891     }
1892     /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1893     @property int progress() {
1894         return getProgress(_maxProgress);
1895     }
1896     /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1897     int getProgress(int maxProgress) {
1898         if (finished)
1899             return maxProgress;
1900         if (_timeElapsed <= 0)
1901             return 0;
1902         return cast(int)(_timeElapsed * maxProgress / _maxInterval);
1903     }
1904     /// Returns true if animation is finished
1905     @property bool finished() {
1906         return _timeElapsed >= _maxInterval; 
1907     }
1908 }
1909 
1910 
1911 /// mixin this to widget class to support tooltips based on widget's action label
1912 mixin template ActionTooltipSupport() {
1913     /// returns true if widget has tooltip to show
1914     override @property bool hasTooltip() {
1915         if (!_action || _action.labelValue.empty)
1916             return false;
1917         return true;
1918     }
1919     /// 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
1920     override Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) {
1921         Widget res = new TextWidget("tooltip", _action.tooltipText);
1922         res.styleId = STYLE_TOOLTIP;
1923         return res;
1924     }
1925 }
1926 
1927 /// use in mixin to set this object property with name propName with value of variable value if variable name matches propName
1928 string generatePropertySetter(string propName) {
1929     return "        if (name.equal(\"" ~ propName ~ "\")) { \n" ~
1930            "            " ~ propName ~ " = value;\n" ~
1931            "            return true;\n" ~
1932            "        }\n";
1933 }
1934 
1935 /// use in mixin to set this object properties with names from parameter list with value of variable value if variable name matches propName
1936 string generatePropertySetters(string[] propNames...) {
1937     string res;
1938     foreach(propName; propNames)
1939         res ~= generatePropertySetter(propName);
1940     return res;
1941 }
1942 
1943 /// 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
1944 string generatePropertySettersMethodOverride(string methodName, string typeName, string[] propNames...) {
1945     string res = "    override bool " ~ methodName ~ "(string name, " ~ typeName ~ " value) {\n" ~
1946                  "        import std.algorithm : equal;\n";
1947     foreach(propName; propNames)
1948         res ~= generatePropertySetter(propName);
1949     res ~= "        return super." ~ methodName ~ "(name, value);\n" ~
1950            "    }\n";
1951     return res;
1952 }
1953 
1954 
1955 __gshared bool TOUCH_MODE = false;