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