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         if(window !is null)
1417             window.update();
1418     }
1419 
1420     /// helper function for implement measure() when widget's content dimensions are known
1421     protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) {
1422         if (visibility == Visibility.Gone) {
1423             _measuredWidth = _measuredHeight = 0;
1424             return;
1425         }
1426         Rect m = margins;
1427         Rect p = padding;
1428         // summarize margins, padding, and content size
1429         int dx = m.left + m.right + p.left + p.right + contentWidth;
1430         int dy = m.top + m.bottom + p.top + p.bottom + contentHeight;
1431         // check for fixed size set in layoutWidth, layoutHeight
1432         int lh = layoutHeight;
1433         int lw = layoutWidth;
1434         // constant value support
1435         if (!(isPercentSize(lh) || isSpecialSize(lh)))
1436             dy = lh.toPixels();
1437         if (!(isPercentSize(lw) || isSpecialSize(lw)))
1438             dx = lw.toPixels();
1439         // apply min/max width and height constraints
1440         int minw = minWidth;
1441         int maxw = maxWidth;
1442         int minh = minHeight;
1443         int maxh = maxHeight;
1444         if (minw != SIZE_UNSPECIFIED && dx < minw)
1445             dx = minw;
1446         if (minh != SIZE_UNSPECIFIED && dy < minh)
1447             dy = minh;
1448         if (maxw != SIZE_UNSPECIFIED && dx > maxw)
1449             dx = maxw;
1450         if (maxh != SIZE_UNSPECIFIED && dy > maxh)
1451             dy = maxh;
1452         // apply FILL_PARENT
1453         //if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT)
1454         //    dx = parentWidth;
1455         //if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT)
1456         //    dy = parentHeight;
1457         // apply max parent size constraint
1458         if (parentWidth != SIZE_UNSPECIFIED && dx > parentWidth)
1459             dx = parentWidth;
1460         if (parentHeight != SIZE_UNSPECIFIED && dy > parentHeight)
1461             dy = parentHeight;
1462         _measuredWidth = dx;
1463         _measuredHeight = dy;
1464     }
1465 
1466     /**
1467         Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
1468 
1469     */
1470     void measure(int parentWidth, int parentHeight) {
1471         measuredContent(parentWidth, parentHeight, 0, 0);
1472     }
1473 
1474     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
1475     void layout(Rect rc) {
1476         if (visibility == Visibility.Gone) {
1477             return;
1478         }
1479         _pos = rc;
1480         _needLayout = false;
1481     }
1482 
1483     /// draws focus rectangle, if enabled in styles
1484     void drawFocusRect(DrawBuf buf, Rect rc) {
1485         const uint[] colors = focusRectColors;
1486         if (colors) {
1487             buf.drawFocusRect(rc, colors);
1488         }
1489     }
1490 
1491     /// Draw widget at its position to buffer
1492     void onDraw(DrawBuf buf) {
1493         if (visibility != Visibility.Visible)
1494             return;
1495         Rect rc = _pos;
1496         applyMargins(rc);
1497         auto saver = ClipRectSaver(buf, rc, alpha);
1498         DrawableRef bg = backgroundDrawable;
1499         if (!bg.isNull) {
1500             bg.drawTo(buf, rc, state);
1501         }
1502         applyPadding(rc);
1503         if (state & State.Focused) {
1504             rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
1505             drawFocusRect(buf, rc);
1506         }
1507         _needDraw = false;
1508     }
1509 
1510     /// Helper function: applies margins to rectangle
1511     void applyMargins(ref Rect rc) {
1512         Rect m = margins;
1513         rc.left += m.left;
1514         rc.top += m.top;
1515         rc.bottom -= m.bottom;
1516         rc.right -= m.right;
1517     }
1518     /// Helper function: applies padding to rectangle
1519     void applyPadding(ref Rect rc) {
1520         Rect m = padding;
1521         rc.left += m.left;
1522         rc.top += m.top;
1523         rc.bottom -= m.bottom;
1524         rc.right -= m.right;
1525     }
1526     /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
1527     static void applyAlign(ref Rect rc, Point sz, Align ha, Align va) {
1528         if (va == Align.Bottom) {
1529             rc.top = rc.bottom - sz.y;
1530         } else if (va == Align.VCenter) {
1531             int dy = (rc.height - sz.y) / 2;
1532             rc.top += dy;
1533             rc.bottom = rc.top + sz.y;
1534         } else {
1535             rc.bottom = rc.top + sz.y;
1536         }
1537         if (ha == Align.Right) {
1538             rc.left = rc.right - sz.x;
1539         } else if (ha == Align.HCenter) {
1540             int dx = (rc.width - sz.x) / 2;
1541             rc.left += dx;
1542             rc.right = rc.left + sz.x;
1543         } else {
1544             rc.right = rc.left + sz.x;
1545         }
1546     }
1547     /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
1548     void applyAlign(ref Rect rc, Point sz) {
1549         Align va = valign;
1550         Align ha = halign;
1551         applyAlign(rc, sz, ha, va);
1552     }
1553 
1554     // ===========================================================
1555     // popup menu support
1556     override bool onMenuItemAction(const Action action) {
1557         return dispatchAction(action);
1558     }
1559 
1560     protected MenuItem _popupMenu;
1561     @property MenuItem popupMenu() { return _popupMenu; }
1562     @property Widget popupMenu(MenuItem popupMenu) {
1563         _popupMenu = popupMenu;
1564         return this;
1565     }
1566 
1567     /// returns true if widget can show popup menu (e.g. by mouse right click at point x,y)
1568     bool canShowPopupMenu(int x, int y) {
1569         if (_popupMenu is null)
1570             return false;
1571         if (_popupMenu.openingSubmenu.assigned)
1572             if (!_popupMenu.openingSubmenu(_popupMenu))
1573                 return false;
1574         return true;
1575     }
1576     /// shows popup menu at (x,y)
1577     void showPopupMenu(int x, int y) {
1578         /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler
1579         if (_popupMenu.openingSubmenu.assigned)
1580             if (!_popupMenu.openingSubmenu(_popupMenu))
1581                 return;
1582         _popupMenu.updateActionState(this);
1583         import dlangui.widgets.popup;
1584         PopupMenu popupMenu = new PopupMenu(_popupMenu);
1585         popupMenu.menuItemAction = this;
1586         PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y);
1587         popup.flags = PopupFlags.CloseOnClickOutside;
1588     }
1589     /// override to change popup menu items state
1590     bool isActionEnabled(const Action action) {
1591         return true;
1592     }
1593 
1594     // ===========================================================
1595     // Widget hierarhy methods
1596 
1597     /// returns number of children of this widget
1598     @property int childCount() const { return 0; }
1599     /// returns child by index
1600     inout(Widget) child(int index) inout { return null; }
1601     /// adds child, returns added item
1602     Widget addChild(Widget item) { assert(false, "addChild: children not suported for this widget type"); }
1603     /// adds child, returns added item
1604     Widget addChildren(Widget[] items) {
1605         foreach(item; items) {
1606             addChild(item);
1607         }
1608         return this;
1609     }
1610     /// inserts child at given index, returns inserted item
1611     Widget insertChild(Widget item, int index) {assert(false, "insertChild: children not suported for this widget type"); }
1612     /// removes child, returns removed item
1613     Widget removeChild(int index) { assert(false, "removeChild: children not suported for this widget type"); }
1614     /// removes child by ID, returns removed item
1615     Widget removeChild(string id) { assert(false, "removeChild: children not suported for this widget type"); }
1616     /// removes child, returns removed item
1617     Widget removeChild(Widget child) { assert(false, "removeChild: children not suported for this widget type"); }
1618     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
1619     int childIndex(Widget item) { return -1; }
1620 
1621 
1622     /// 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).
1623     bool isChild(Widget item, bool deepSearch = true) {
1624         if (deepSearch) {
1625             // this widget or some widget inside children tree
1626             if (item is this)
1627                 return true;
1628             for (int i = 0; i < childCount; i++) {
1629                 if (child(i).isChild(item))
1630                     return true;
1631             }
1632         } else {
1633             // only one of children
1634             for (int i = 0; i < childCount; i++) {
1635                 if (item is child(i))
1636                     return true;
1637             }
1638         }
1639         return false;
1640     }
1641 
1642     /// find child of specified type T by id, returns null if not found or cannot be converted to type T
1643     T childById(T = typeof(this))(string id, bool deepSearch = true) {
1644         if (deepSearch) {
1645             // search everywhere inside child tree
1646             if (compareId(id)) {
1647                 T found = cast(T)this;
1648                 if (found)
1649                     return found;
1650             }
1651             // lookup children
1652             for (int i = childCount - 1; i >= 0; i--) {
1653                 Widget res = child(i).childById(id);
1654                 if (res !is null) {
1655                     T found = cast(T)res;
1656                     if (found)
1657                         return found;
1658                 }
1659             }
1660         } else {
1661             // search only across children of this widget
1662             for (int i = childCount - 1; i >= 0; i--) {
1663                 Widget w = child(i);
1664                 if (id.equal(w.id)) {
1665                     T found = cast(T)w;
1666                     if (found)
1667                         return found;
1668                 }
1669             }
1670         }
1671         // not found
1672         return null;
1673     }
1674 
1675     /// returns parent widget, null for top level widget
1676     @property Widget parent() const { return _parent ? cast(Widget)_parent : null; }
1677     /// sets parent for widget
1678     @property Widget parent(Widget parent) { _parent = parent; return this; }
1679     /// returns window (if widget or its parent is attached to window)
1680     @property Window window() const {
1681         Widget p = cast(Widget)this;
1682         while (p !is null) {
1683             if (p._window !is null)
1684                 return cast(Window)p._window;
1685             p = p.parent;
1686         }
1687         return null;
1688     }
1689     /// sets window (to be used for top level widget from Window implementation). TODO: hide it from API?
1690     @property void window(Window window) {
1691         _window = window;
1692     }
1693 
1694     void removeAllChildren(bool destroyObj = true) {
1695         // override
1696     }
1697 
1698     /// set string property value, for ML loaders
1699     bool setStringProperty(string name, string value) {
1700         mixin(generatePropertySetters("id", "styleId", "backgroundImageId", "backgroundColor", "textColor", "fontFace"));
1701         if (name.equal("text")) {
1702             text = UIString.fromId(value);
1703             return true;
1704         }
1705         if (name.equal("tooltipText")) {
1706             tooltipText = UIString.fromId(value);
1707             return true;
1708         }
1709         return false;
1710     }
1711 
1712     /// set string property value, for ML loaders
1713     bool setDstringProperty(string name, dstring value) {
1714         if (name.equal("text")) {
1715             text = UIString.fromRaw(value);
1716             return true;
1717         }
1718         if (name.equal("tooltipText")) {
1719             tooltipText = UIString.fromRaw(value);
1720             return true;
1721         }
1722         return false;
1723     }
1724 
1725     /// set string property value, for ML loaders
1726     bool setUistringProperty(string name, UIString value) {
1727         if (name.equal("text")) {
1728             text = value;
1729             return true;
1730         }
1731         if (name.equal("tooltipText")) {
1732             tooltipText = value;
1733             return true;
1734         }
1735         return false;
1736     }
1737 
1738     /// StringListValue list values
1739     bool setStringListValueListProperty(string propName, StringListValue[] values) {
1740         return false;
1741     }
1742 
1743     /// UIString list values
1744     bool setUIStringListProperty(string propName, UIString[] values) {
1745         return false;
1746     }
1747 
1748     /// set string property value, for ML loaders
1749     bool setBoolProperty(string name, bool value) {
1750         mixin(generatePropertySetters("enabled", "clickable", "checkable", "focusable", "checked", "fontItalic"));
1751         return false;
1752     }
1753 
1754     /// set double property value, for ML loaders
1755     bool setDoubleProperty(string name, double value) {
1756         if (name.equal("alpha")) {
1757             int n = cast(int)(value * 255);
1758             return setIntProperty(name, n);
1759         }
1760         return false;
1761     }
1762 
1763     /// set int property value, for ML loaders
1764     bool setIntProperty(string name, int value) {
1765         if (name.equal("alpha")) {
1766             if (value < 0)
1767                 value = 0;
1768             else if (value > 255)
1769                 value = 255;
1770             alpha = cast(ushort)value;
1771             return true;
1772         }
1773         mixin(generatePropertySetters("minWidth", "maxWidth", "minHeight", "maxHeight", "layoutWidth", "layoutHeight", "layoutWeight", "textColor", "backgroundColor", "fontSize", "fontWeight"));
1774         if (name.equal("margins")) { // use same value for all sides
1775             margins = Rect(value, value, value, value);
1776             return true;
1777         }
1778         if (name.equal("alignment")) {
1779             alignment = cast(Align)value;
1780             return true;
1781         }
1782         if (name.equal("padding")) { // use same value for all sides
1783             padding = Rect(value, value, value, value);
1784             return true;
1785         }
1786         return false;
1787     }
1788 
1789     /// set Rect property value, for ML loaders
1790     bool setRectProperty(string name, Rect value) {
1791         mixin(generatePropertySetters("margins", "padding"));
1792         return false;
1793     }
1794 
1795 private:
1796     bool nextClickIsDouble = false;
1797 }
1798 
1799 /** Widget list holder. */
1800 alias WidgetList = ObjectList!Widget;
1801 
1802 /** Base class for widgets which have children. */
1803 class WidgetGroup : Widget {
1804 
1805     /// empty parameter list constructor - for usage by factory
1806     this() {
1807         this(null);
1808     }
1809     /// create with ID parameter
1810     this(string ID) {
1811         super(ID);
1812     }
1813 
1814     protected WidgetList _children;
1815 
1816     override @property bool needDraw()
1817     {
1818         return _children.asArray.any!(x => x.needDraw) || _needDraw;
1819     }
1820 
1821     /// returns number of children of this widget
1822     @property override int childCount() const { return _children.count; }
1823     /// returns child by index
1824     override inout(Widget) child(int index) inout { return _children.get(index); }
1825     /// adds child, returns added item
1826     override Widget addChild(Widget item) { return _children.add(item).parent(this); }
1827     /// inserts child at given index, returns inserted item
1828     override Widget insertChild(Widget item, int index) { return _children.insert(item,index).parent(this); }
1829     /// removes child, returns removed item
1830     override Widget removeChild(int index) {
1831         Widget res = _children.remove(index);
1832         if (res !is null)
1833             res.parent = null;
1834         return res;
1835     }
1836     /// removes child by ID, returns removed item
1837     override Widget removeChild(string ID) {
1838         Widget res = null;
1839         int index = _children.indexOf(ID);
1840         if (index < 0)
1841             return null;
1842         res = _children.remove(index);
1843         if (res !is null)
1844             res.parent = null;
1845         return res;
1846     }
1847     /// removes child, returns removed item
1848     override Widget removeChild(Widget child) {
1849         Widget res = null;
1850         int index = _children.indexOf(child);
1851         if (index < 0)
1852             return null;
1853         res = _children.remove(index);
1854         if (res !is null)
1855             res.parent = null;
1856         return res;
1857     }
1858     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
1859     override int childIndex(Widget item) { return _children.indexOf(item); }
1860 
1861 	override void removeAllChildren(bool destroyObj = true) {
1862         _children.clear(destroyObj);
1863     }
1864 
1865     /// replace child with other child
1866     void replaceChild(Widget newChild, Widget oldChild) {
1867         _children.replace(newChild, oldChild);
1868     }
1869 
1870 }
1871 
1872 /** WidgetGroup with default drawing of children (just draw all children) */
1873 class WidgetGroupDefaultDrawing : WidgetGroup {
1874     /// empty parameter list constructor - for usage by factory
1875     this() {
1876         this(null);
1877     }
1878     /// create with ID parameter
1879     this(string ID) {
1880         super(ID);
1881     }
1882     /// Draw widget at its position to buffer
1883     override void onDraw(DrawBuf buf) {
1884         if (visibility != Visibility.Visible)
1885             return;
1886         super.onDraw(buf);
1887         Rect rc = _pos;
1888         applyMargins(rc);
1889         applyPadding(rc);
1890         auto saver = ClipRectSaver(buf, rc);
1891         for (int i = 0; i < _children.count; i++) {
1892             Widget item = _children.get(i);
1893             item.onDraw(buf);
1894         }
1895     }
1896 }
1897 
1898 /// helper for locating items in list, tree, table or other controls by typing their name
1899 struct TextTypingShortcutHelper {
1900     int timeoutMillis = 800; // expiration time for entered text; after timeout collected text will be cleared
1901     private long _lastUpdateTimeStamp;
1902     private dchar[] _text;
1903     /// cancel text collection (next typed text will be collected from scratch)
1904     void cancel() {
1905         _text.length = 0;
1906         _lastUpdateTimeStamp = 0;
1907     }
1908     /// returns collected text string - use it for lookup
1909     @property dstring text() { return _text.dup; }
1910     /// pass key event here; returns true if search text is updated and you can move selection using it
1911     bool onKeyEvent(KeyEvent event) {
1912         long ts = currentTimeMillis;
1913         if (_lastUpdateTimeStamp && ts - _lastUpdateTimeStamp > timeoutMillis)
1914             cancel();
1915         if (event.action == KeyAction.Text) {
1916             _text ~= event.text;
1917             _lastUpdateTimeStamp = ts;
1918             return _text.length > 0;
1919         }
1920         if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
1921             switch (event.keyCode) with (KeyCode) {
1922                 case LEFT:
1923                 case RIGHT:
1924                 case UP:
1925                 case DOWN:
1926                 case HOME:
1927                 case END:
1928                 case TAB:
1929                 case PAGEUP:
1930                 case PAGEDOWN:
1931                 case BACK:
1932                     cancel();
1933                     break;
1934                 default:
1935                     break;
1936             }
1937         }
1938         return false;
1939     }
1940 
1941     /// cancel text typing on some mouse events, if necessary
1942     void onMouseEvent(MouseEvent event) {
1943         if (event.action == MouseAction.ButtonUp || event.action == MouseAction.ButtonDown)
1944             cancel();
1945     }
1946 }
1947 
1948 
1949 enum ONE_SECOND = 10_000_000L;
1950 
1951 /// Helper to handle animation progress
1952 struct AnimationHelper {
1953     private long _timeElapsed;
1954     private long _maxInterval;
1955     private int  _maxProgress;
1956 
1957     /// start new animation interval
1958     void start(long maxInterval, int maxProgress) {
1959         _timeElapsed = 0;
1960         _maxInterval = maxInterval;
1961         _maxProgress = maxProgress;
1962         assert(_maxInterval > 0);
1963         assert(_maxProgress > 0);
1964     }
1965     /// Adds elapsed time; returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1966     int animate(long time) {
1967         _timeElapsed += time;
1968         return progress();
1969     }
1970     /// restart with same max interval and progress
1971     void restart() {
1972         if (!_maxInterval) {
1973             _maxInterval = ONE_SECOND;
1974         }
1975         _timeElapsed = 0;
1976     }
1977     /// returns time elapsed since start
1978     @property long elapsed() {
1979         return _timeElapsed;
1980     }
1981     /// get current time interval
1982     @property long interval() {
1983         return _maxInterval;
1984     }
1985     /// override current time interval, retaining the same progress %
1986     @property void interval(long newInterval) {
1987         int p = getProgress(10000);
1988         _maxInterval = newInterval;
1989         _timeElapsed = p * newInterval / 10000;
1990     }
1991     /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1992     @property int progress() {
1993         return getProgress(_maxProgress);
1994     }
1995     /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress
1996     int getProgress(int maxProgress) {
1997         if (finished)
1998             return maxProgress;
1999         if (_timeElapsed <= 0)
2000             return 0;
2001         return cast(int)(_timeElapsed * maxProgress / _maxInterval);
2002     }
2003     /// Returns true if animation is finished
2004     @property bool finished() {
2005         return _timeElapsed >= _maxInterval;
2006     }
2007 }
2008 
2009 
2010 /// mixin this to widget class to support tooltips based on widget's action label
2011 mixin template ActionTooltipSupport() {
2012     /// returns true if widget has tooltip to show
2013     override @property bool hasTooltip() {
2014         if (!_action || _action.labelValue.empty)
2015             return false;
2016         return true;
2017     }
2018     /// 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
2019     override Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) {
2020         Widget res = new TextWidget("tooltip", _action.tooltipText);
2021         res.styleId = STYLE_TOOLTIP;
2022         return res;
2023     }
2024 }
2025 
2026 /// use in mixin to set this object property with name propName with value of variable value if variable name matches propName
2027 string generatePropertySetter(string propName) {
2028     return "        if (name.equal(\"" ~ propName ~ "\")) { \n" ~
2029            "            " ~ propName ~ " = value;\n" ~
2030            "            return true;\n" ~
2031            "        }\n";
2032 }
2033 
2034 /// use in mixin to set this object properties with names from parameter list with value of variable value if variable name matches propName
2035 string generatePropertySetters(string[] propNames...) {
2036     string res;
2037     foreach(propName; propNames)
2038         res ~= generatePropertySetter(propName);
2039     return res;
2040 }
2041 
2042 /// 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
2043 string generatePropertySettersMethodOverride(string methodName, string typeName, string[] propNames...) {
2044     string res = "    override bool " ~ methodName ~ "(string name, " ~ typeName ~ " value) {\n" ~
2045                  "        import std.algorithm : equal;\n";
2046     foreach(propName; propNames)
2047         res ~= generatePropertySetter(propName);
2048     res ~= "        return super." ~ methodName ~ "(name, value);\n" ~
2049            "    }\n";
2050     return res;
2051 }
2052 
2053 
2054 __gshared bool TOUCH_MODE = false;