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