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.tryGet(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;