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