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