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 } 1417 1418 /// helper function for implement measure() when widget's content dimensions are known 1419 protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) { 1420 if (visibility == Visibility.Gone) { 1421 _measuredWidth = _measuredHeight = 0; 1422 return; 1423 } 1424 Rect m = margins; 1425 Rect p = padding; 1426 // summarize margins, padding, and content size 1427 int dx = m.left + m.right + p.left + p.right + contentWidth; 1428 int dy = m.top + m.bottom + p.top + p.bottom + contentHeight; 1429 // check for fixed size set in layoutWidth, layoutHeight 1430 int lh = layoutHeight; 1431 int lw = layoutWidth; 1432 // constant value support 1433 if (!(isPercentSize(lh) || isSpecialSize(lh))) 1434 dy = lh.toPixels(); 1435 if (!(isPercentSize(lw) || isSpecialSize(lw))) 1436 dx = lw.toPixels(); 1437 // apply min/max width and height constraints 1438 int minw = minWidth; 1439 int maxw = maxWidth; 1440 int minh = minHeight; 1441 int maxh = maxHeight; 1442 if (minw != SIZE_UNSPECIFIED && dx < minw) 1443 dx = minw; 1444 if (minh != SIZE_UNSPECIFIED && dy < minh) 1445 dy = minh; 1446 if (maxw != SIZE_UNSPECIFIED && dx > maxw) 1447 dx = maxw; 1448 if (maxh != SIZE_UNSPECIFIED && dy > maxh) 1449 dy = maxh; 1450 // apply FILL_PARENT 1451 //if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT) 1452 // dx = parentWidth; 1453 //if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT) 1454 // dy = parentHeight; 1455 // apply max parent size constraint 1456 if (parentWidth != SIZE_UNSPECIFIED && dx > parentWidth) 1457 dx = parentWidth; 1458 if (parentHeight != SIZE_UNSPECIFIED && dy > parentHeight) 1459 dy = parentHeight; 1460 _measuredWidth = dx; 1461 _measuredHeight = dy; 1462 } 1463 1464 /** 1465 Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 1466 1467 */ 1468 void measure(int parentWidth, int parentHeight) { 1469 measuredContent(parentWidth, parentHeight, 0, 0); 1470 } 1471 1472 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 1473 void layout(Rect rc) { 1474 if (visibility == Visibility.Gone) { 1475 return; 1476 } 1477 _pos = rc; 1478 _needLayout = false; 1479 } 1480 1481 /// draws focus rectangle, if enabled in styles 1482 void drawFocusRect(DrawBuf buf, Rect rc) { 1483 const uint[] colors = focusRectColors; 1484 if (colors) { 1485 buf.drawFocusRect(rc, colors); 1486 } 1487 } 1488 1489 /// Draw widget at its position to buffer 1490 void onDraw(DrawBuf buf) { 1491 if (visibility != Visibility.Visible) 1492 return; 1493 Rect rc = _pos; 1494 applyMargins(rc); 1495 auto saver = ClipRectSaver(buf, rc, alpha); 1496 DrawableRef bg = backgroundDrawable; 1497 if (!bg.isNull) { 1498 bg.drawTo(buf, rc, state); 1499 } 1500 applyPadding(rc); 1501 if (state & State.Focused) { 1502 rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING); 1503 drawFocusRect(buf, rc); 1504 } 1505 _needDraw = false; 1506 } 1507 1508 /// Helper function: applies margins to rectangle 1509 void applyMargins(ref Rect rc) { 1510 Rect m = margins; 1511 rc.left += m.left; 1512 rc.top += m.top; 1513 rc.bottom -= m.bottom; 1514 rc.right -= m.right; 1515 } 1516 /// Helper function: applies padding to rectangle 1517 void applyPadding(ref Rect rc) { 1518 Rect m = padding; 1519 rc.left += m.left; 1520 rc.top += m.top; 1521 rc.bottom -= m.bottom; 1522 rc.right -= m.right; 1523 } 1524 /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc. 1525 static void applyAlign(ref Rect rc, Point sz, Align ha, Align va) { 1526 if (va == Align.Bottom) { 1527 rc.top = rc.bottom - sz.y; 1528 } else if (va == Align.VCenter) { 1529 int dy = (rc.height - sz.y) / 2; 1530 rc.top += dy; 1531 rc.bottom = rc.top + sz.y; 1532 } else { 1533 rc.bottom = rc.top + sz.y; 1534 } 1535 if (ha == Align.Right) { 1536 rc.left = rc.right - sz.x; 1537 } else if (ha == Align.HCenter) { 1538 int dx = (rc.width - sz.x) / 2; 1539 rc.left += dx; 1540 rc.right = rc.left + sz.x; 1541 } else { 1542 rc.right = rc.left + sz.x; 1543 } 1544 } 1545 /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc. 1546 void applyAlign(ref Rect rc, Point sz) { 1547 Align va = valign; 1548 Align ha = halign; 1549 applyAlign(rc, sz, ha, va); 1550 } 1551 1552 // =========================================================== 1553 // popup menu support 1554 override bool onMenuItemAction(const Action action) { 1555 return dispatchAction(action); 1556 } 1557 1558 protected MenuItem _popupMenu; 1559 @property MenuItem popupMenu() { return _popupMenu; } 1560 @property Widget popupMenu(MenuItem popupMenu) { 1561 _popupMenu = popupMenu; 1562 return this; 1563 } 1564 1565 /// returns true if widget can show popup menu (e.g. by mouse right click at point x,y) 1566 bool canShowPopupMenu(int x, int y) { 1567 if (_popupMenu is null) 1568 return false; 1569 if (_popupMenu.openingSubmenu.assigned) 1570 if (!_popupMenu.openingSubmenu(_popupMenu)) 1571 return false; 1572 return true; 1573 } 1574 /// shows popup menu at (x,y) 1575 void showPopupMenu(int x, int y) { 1576 /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler 1577 if (_popupMenu.openingSubmenu.assigned) 1578 if (!_popupMenu.openingSubmenu(_popupMenu)) 1579 return; 1580 _popupMenu.updateActionState(this); 1581 import dlangui.widgets.popup; 1582 PopupMenu popupMenu = new PopupMenu(_popupMenu); 1583 popupMenu.menuItemAction = this; 1584 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y); 1585 popup.flags = PopupFlags.CloseOnClickOutside; 1586 } 1587 /// override to change popup menu items state 1588 bool isActionEnabled(const Action action) { 1589 return true; 1590 } 1591 1592 // =========================================================== 1593 // Widget hierarhy methods 1594 1595 /// returns number of children of this widget 1596 @property int childCount() const { return 0; } 1597 /// returns child by index 1598 inout(Widget) child(int index) inout { return null; } 1599 /// adds child, returns added item 1600 Widget addChild(Widget item) { assert(false, "addChild: children not suported for this widget type"); } 1601 /// adds child, returns added item 1602 Widget addChildren(Widget[] items) { 1603 foreach(item; items) { 1604 addChild(item); 1605 } 1606 return this; 1607 } 1608 /// inserts child at given index, returns inserted item 1609 Widget insertChild(Widget item, int index) {assert(false, "insertChild: children not suported for this widget type"); } 1610 /// removes child, returns removed item 1611 Widget removeChild(int index) { assert(false, "removeChild: children not suported for this widget type"); } 1612 /// removes child by ID, returns removed item 1613 Widget removeChild(string id) { assert(false, "removeChild: children not suported for this widget type"); } 1614 /// removes child, returns removed item 1615 Widget removeChild(Widget child) { assert(false, "removeChild: children not suported for this widget type"); } 1616 /// returns index of widget in child list, -1 if passed widget is not a child of this widget 1617 int childIndex(Widget item) { return -1; } 1618 1619 1620 /// 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). 1621 bool isChild(Widget item, bool deepSearch = true) { 1622 if (deepSearch) { 1623 // this widget or some widget inside children tree 1624 if (item is this) 1625 return true; 1626 for (int i = 0; i < childCount; i++) { 1627 if (child(i).isChild(item)) 1628 return true; 1629 } 1630 } else { 1631 // only one of children 1632 for (int i = 0; i < childCount; i++) { 1633 if (item is child(i)) 1634 return true; 1635 } 1636 } 1637 return false; 1638 } 1639 1640 /// find child of specified type T by id, returns null if not found or cannot be converted to type T 1641 T childById(T = typeof(this))(string id, bool deepSearch = true) { 1642 if (deepSearch) { 1643 // search everywhere inside child tree 1644 if (compareId(id)) { 1645 T found = cast(T)this; 1646 if (found) 1647 return found; 1648 } 1649 // lookup children 1650 for (int i = childCount - 1; i >= 0; i--) { 1651 Widget res = child(i).childById(id); 1652 if (res !is null) { 1653 T found = cast(T)res; 1654 if (found) 1655 return found; 1656 } 1657 } 1658 } else { 1659 // search only across children of this widget 1660 for (int i = childCount - 1; i >= 0; i--) { 1661 Widget w = child(i); 1662 if (id.equal(w.id)) { 1663 T found = cast(T)w; 1664 if (found) 1665 return found; 1666 } 1667 } 1668 } 1669 // not found 1670 return null; 1671 } 1672 1673 /// returns parent widget, null for top level widget 1674 @property Widget parent() const { return _parent ? cast(Widget)_parent : null; } 1675 /// sets parent for widget 1676 @property Widget parent(Widget parent) { _parent = parent; return this; } 1677 /// returns window (if widget or its parent is attached to window) 1678 @property Window window() const { 1679 Widget p = cast(Widget)this; 1680 while (p !is null) { 1681 if (p._window !is null) 1682 return cast(Window)p._window; 1683 p = p.parent; 1684 } 1685 return null; 1686 } 1687 /// sets window (to be used for top level widget from Window implementation). TODO: hide it from API? 1688 @property void window(Window window) { 1689 _window = window; 1690 } 1691 1692 void removeAllChildren(bool destroyObj = true) { 1693 // override 1694 } 1695 1696 /// set string property value, for ML loaders 1697 bool setStringProperty(string name, string value) { 1698 mixin(generatePropertySetters("id", "styleId", "backgroundImageId", "backgroundColor", "textColor", "fontFace")); 1699 if (name.equal("text")) { 1700 text = UIString.fromId(value); 1701 return true; 1702 } 1703 if (name.equal("tooltipText")) { 1704 tooltipText = UIString.fromId(value); 1705 return true; 1706 } 1707 return false; 1708 } 1709 1710 /// set string property value, for ML loaders 1711 bool setDstringProperty(string name, dstring value) { 1712 if (name.equal("text")) { 1713 text = UIString.fromRaw(value); 1714 return true; 1715 } 1716 if (name.equal("tooltipText")) { 1717 tooltipText = UIString.fromRaw(value); 1718 return true; 1719 } 1720 return false; 1721 } 1722 1723 /// set string property value, for ML loaders 1724 bool setUistringProperty(string name, UIString value) { 1725 if (name.equal("text")) { 1726 text = value; 1727 return true; 1728 } 1729 if (name.equal("tooltipText")) { 1730 tooltipText = value; 1731 return true; 1732 } 1733 return false; 1734 } 1735 1736 /// StringListValue list values 1737 bool setStringListValueListProperty(string propName, StringListValue[] values) { 1738 return false; 1739 } 1740 1741 /// UIString list values 1742 bool setUIStringListProperty(string propName, UIString[] values) { 1743 return false; 1744 } 1745 1746 /// set string property value, for ML loaders 1747 bool setBoolProperty(string name, bool value) { 1748 mixin(generatePropertySetters("enabled", "clickable", "checkable", "focusable", "checked", "fontItalic")); 1749 return false; 1750 } 1751 1752 /// set double property value, for ML loaders 1753 bool setDoubleProperty(string name, double value) { 1754 if (name.equal("alpha")) { 1755 int n = cast(int)(value * 255); 1756 return setIntProperty(name, n); 1757 } 1758 return false; 1759 } 1760 1761 /// set int property value, for ML loaders 1762 bool setIntProperty(string name, int value) { 1763 if (name.equal("alpha")) { 1764 if (value < 0) 1765 value = 0; 1766 else if (value > 255) 1767 value = 255; 1768 alpha = cast(ushort)value; 1769 return true; 1770 } 1771 mixin(generatePropertySetters("minWidth", "maxWidth", "minHeight", "maxHeight", "layoutWidth", "layoutHeight", "layoutWeight", "textColor", "backgroundColor", "fontSize", "fontWeight")); 1772 if (name.equal("margins")) { // use same value for all sides 1773 margins = Rect(value, value, value, value); 1774 return true; 1775 } 1776 if (name.equal("alignment")) { 1777 alignment = cast(Align)value; 1778 return true; 1779 } 1780 if (name.equal("padding")) { // use same value for all sides 1781 padding = Rect(value, value, value, value); 1782 return true; 1783 } 1784 return false; 1785 } 1786 1787 /// set Rect property value, for ML loaders 1788 bool setRectProperty(string name, Rect value) { 1789 mixin(generatePropertySetters("margins", "padding")); 1790 return false; 1791 } 1792 1793 private: 1794 bool nextClickIsDouble = false; 1795 } 1796 1797 /** Widget list holder. */ 1798 alias WidgetList = ObjectList!Widget; 1799 1800 /** Base class for widgets which have children. */ 1801 class WidgetGroup : Widget { 1802 1803 /// empty parameter list constructor - for usage by factory 1804 this() { 1805 this(null); 1806 } 1807 /// create with ID parameter 1808 this(string ID) { 1809 super(ID); 1810 } 1811 1812 protected WidgetList _children; 1813 1814 /// returns number of children of this widget 1815 @property override int childCount() const { return _children.count; } 1816 /// returns child by index 1817 override inout(Widget) child(int index) inout { return _children.get(index); } 1818 /// adds child, returns added item 1819 override Widget addChild(Widget item) { return _children.add(item).parent(this); } 1820 /// inserts child at given index, returns inserted item 1821 override Widget insertChild(Widget item, int index) { return _children.insert(item,index).parent(this); } 1822 /// removes child, returns removed item 1823 override Widget removeChild(int index) { 1824 Widget res = _children.remove(index); 1825 if (res !is null) 1826 res.parent = null; 1827 return res; 1828 } 1829 /// removes child by ID, returns removed item 1830 override Widget removeChild(string ID) { 1831 Widget res = null; 1832 int index = _children.indexOf(ID); 1833 if (index < 0) 1834 return null; 1835 res = _children.remove(index); 1836 if (res !is null) 1837 res.parent = null; 1838 return res; 1839 } 1840 /// removes child, returns removed item 1841 override Widget removeChild(Widget child) { 1842 Widget res = null; 1843 int index = _children.indexOf(child); 1844 if (index < 0) 1845 return null; 1846 res = _children.remove(index); 1847 if (res !is null) 1848 res.parent = null; 1849 return res; 1850 } 1851 /// returns index of widget in child list, -1 if passed widget is not a child of this widget 1852 override int childIndex(Widget item) { return _children.indexOf(item); } 1853 1854 override void removeAllChildren(bool destroyObj = true) { 1855 _children.clear(destroyObj); 1856 } 1857 1858 /// replace child with other child 1859 void replaceChild(Widget newChild, Widget oldChild) { 1860 _children.replace(newChild, oldChild); 1861 } 1862 1863 } 1864 1865 /** WidgetGroup with default drawing of children (just draw all children) */ 1866 class WidgetGroupDefaultDrawing : WidgetGroup { 1867 /// empty parameter list constructor - for usage by factory 1868 this() { 1869 this(null); 1870 } 1871 /// create with ID parameter 1872 this(string ID) { 1873 super(ID); 1874 } 1875 /// Draw widget at its position to buffer 1876 override void onDraw(DrawBuf buf) { 1877 if (visibility != Visibility.Visible) 1878 return; 1879 super.onDraw(buf); 1880 Rect rc = _pos; 1881 applyMargins(rc); 1882 applyPadding(rc); 1883 auto saver = ClipRectSaver(buf, rc); 1884 for (int i = 0; i < _children.count; i++) { 1885 Widget item = _children.get(i); 1886 item.onDraw(buf); 1887 } 1888 } 1889 } 1890 1891 /// helper for locating items in list, tree, table or other controls by typing their name 1892 struct TextTypingShortcutHelper { 1893 int timeoutMillis = 800; // expiration time for entered text; after timeout collected text will be cleared 1894 private long _lastUpdateTimeStamp; 1895 private dchar[] _text; 1896 /// cancel text collection (next typed text will be collected from scratch) 1897 void cancel() { 1898 _text.length = 0; 1899 _lastUpdateTimeStamp = 0; 1900 } 1901 /// returns collected text string - use it for lookup 1902 @property dstring text() { return _text.dup; } 1903 /// pass key event here; returns true if search text is updated and you can move selection using it 1904 bool onKeyEvent(KeyEvent event) { 1905 long ts = currentTimeMillis; 1906 if (_lastUpdateTimeStamp && ts - _lastUpdateTimeStamp > timeoutMillis) 1907 cancel(); 1908 if (event.action == KeyAction.Text) { 1909 _text ~= event.text; 1910 _lastUpdateTimeStamp = ts; 1911 return _text.length > 0; 1912 } 1913 if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) { 1914 switch (event.keyCode) with (KeyCode) { 1915 case LEFT: 1916 case RIGHT: 1917 case UP: 1918 case DOWN: 1919 case HOME: 1920 case END: 1921 case TAB: 1922 case PAGEUP: 1923 case PAGEDOWN: 1924 case BACK: 1925 cancel(); 1926 break; 1927 default: 1928 break; 1929 } 1930 } 1931 return false; 1932 } 1933 1934 /// cancel text typing on some mouse events, if necessary 1935 void onMouseEvent(MouseEvent event) { 1936 if (event.action == MouseAction.ButtonUp || event.action == MouseAction.ButtonDown) 1937 cancel(); 1938 } 1939 } 1940 1941 1942 enum ONE_SECOND = 10_000_000L; 1943 1944 /// Helper to handle animation progress 1945 struct AnimationHelper { 1946 private long _timeElapsed; 1947 private long _maxInterval; 1948 private int _maxProgress; 1949 1950 /// start new animation interval 1951 void start(long maxInterval, int maxProgress) { 1952 _timeElapsed = 0; 1953 _maxInterval = maxInterval; 1954 _maxProgress = maxProgress; 1955 assert(_maxInterval > 0); 1956 assert(_maxProgress > 0); 1957 } 1958 /// Adds elapsed time; returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress 1959 int animate(long time) { 1960 _timeElapsed += time; 1961 return progress(); 1962 } 1963 /// restart with same max interval and progress 1964 void restart() { 1965 if (!_maxInterval) { 1966 _maxInterval = ONE_SECOND; 1967 } 1968 _timeElapsed = 0; 1969 } 1970 /// returns time elapsed since start 1971 @property long elapsed() { 1972 return _timeElapsed; 1973 } 1974 /// get current time interval 1975 @property long interval() { 1976 return _maxInterval; 1977 } 1978 /// override current time interval, retaining the same progress % 1979 @property void interval(long newInterval) { 1980 int p = getProgress(10000); 1981 _maxInterval = newInterval; 1982 _timeElapsed = p * newInterval / 10000; 1983 } 1984 /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress 1985 @property int progress() { 1986 return getProgress(_maxProgress); 1987 } 1988 /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress 1989 int getProgress(int maxProgress) { 1990 if (finished) 1991 return maxProgress; 1992 if (_timeElapsed <= 0) 1993 return 0; 1994 return cast(int)(_timeElapsed * maxProgress / _maxInterval); 1995 } 1996 /// Returns true if animation is finished 1997 @property bool finished() { 1998 return _timeElapsed >= _maxInterval; 1999 } 2000 } 2001 2002 2003 /// mixin this to widget class to support tooltips based on widget's action label 2004 mixin template ActionTooltipSupport() { 2005 /// returns true if widget has tooltip to show 2006 override @property bool hasTooltip() { 2007 if (!_action || _action.labelValue.empty) 2008 return false; 2009 return true; 2010 } 2011 /// 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 2012 override Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) { 2013 Widget res = new TextWidget("tooltip", _action.tooltipText); 2014 res.styleId = STYLE_TOOLTIP; 2015 return res; 2016 } 2017 } 2018 2019 /// use in mixin to set this object property with name propName with value of variable value if variable name matches propName 2020 string generatePropertySetter(string propName) { 2021 return " if (name.equal(\"" ~ propName ~ "\")) { \n" ~ 2022 " " ~ propName ~ " = value;\n" ~ 2023 " return true;\n" ~ 2024 " }\n"; 2025 } 2026 2027 /// use in mixin to set this object properties with names from parameter list with value of variable value if variable name matches propName 2028 string generatePropertySetters(string[] propNames...) { 2029 string res; 2030 foreach(propName; propNames) 2031 res ~= generatePropertySetter(propName); 2032 return res; 2033 } 2034 2035 /// 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 2036 string generatePropertySettersMethodOverride(string methodName, string typeName, string[] propNames...) { 2037 string res = " override bool " ~ methodName ~ "(string name, " ~ typeName ~ " value) {\n" ~ 2038 " import std.algorithm : equal;\n"; 2039 foreach(propName; propNames) 2040 res ~= generatePropertySetter(propName); 2041 res ~= " return super." ~ methodName ~ "(name, value);\n" ~ 2042 " }\n"; 2043 return res; 2044 } 2045 2046 2047 __gshared bool TOUCH_MODE = false;