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