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