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