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) {
667 return _pos.isPointInside(x, y);
668 }
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;