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 return window.setFocus(this, reason); 1103 } 1104 /// searches children for first focusable item, returns null if not found 1105 Widget findFocusableChild(bool defaultOnly) { 1106 for(int i = 0; i < childCount; i++) { 1107 Widget w = child(i); 1108 if (w.canFocus && (!defaultOnly || (w.state & State.Default) != 0)) 1109 return w; 1110 w = w.findFocusableChild(defaultOnly); 1111 if (w !is null) 1112 return w; 1113 } 1114 if (canFocus) 1115 return this; 1116 return null; 1117 } 1118 1119 // ======================================================= 1120 // Events 1121 1122 protected ActionMap _acceleratorMap; 1123 @property ref ActionMap acceleratorMap() { return _acceleratorMap; } 1124 1125 /// override to handle specific actions 1126 bool handleAction(const Action a) { 1127 if (onAction.assigned) 1128 if (onAction(this, a)) 1129 return true; 1130 return false; 1131 } 1132 /// override to handle specific actions state (e.g. change enabled state for supported actions) 1133 bool handleActionStateRequest(const Action a) { 1134 return false; 1135 } 1136 1137 /// call to dispatch action 1138 bool dispatchAction(const Action a) { 1139 if (window) 1140 return window.dispatchAction(a, this); 1141 else 1142 return handleAction(a); 1143 } 1144 1145 // called to process click and notify listeners 1146 protected bool handleClick() { 1147 bool res = false; 1148 if (click.assigned) 1149 res = click(this); 1150 else if (_action) { 1151 return dispatchAction(_action); 1152 } 1153 return res; 1154 } 1155 1156 1157 void cancelLayout() { 1158 _needLayout = false; 1159 } 1160 1161 /// set new timer to call onTimer() after specified interval (for recurred notifications, return true from onTimer) 1162 ulong setTimer(long intervalMillis) { 1163 if (auto w = window) 1164 return w.setTimer(this, intervalMillis); 1165 return 0; // no window - no timer 1166 } 1167 1168 /// cancel timer - pass value returned from setTimer() as timerId parameter 1169 void cancelTimer(ulong timerId) { 1170 if (auto w = window) 1171 w.cancelTimer(timerId); 1172 } 1173 1174 /// handle timer; return true to repeat timer event after next interval, false cancel timer 1175 bool onTimer(ulong id) { 1176 // override to do something useful 1177 // return true to repeat after the same interval, false to stop timer 1178 return false; 1179 } 1180 1181 /// map key to action 1182 Action findKeyAction(uint keyCode, uint flags) { 1183 Action action = _acceleratorMap.findByKey(keyCode, flags); 1184 if (action) 1185 return action; 1186 if (keyToAction.assigned) 1187 action = keyToAction(this, keyCode, flags); 1188 return action; 1189 } 1190 1191 /// process key event, return true if event is processed. 1192 bool onKeyEvent(KeyEvent event) { 1193 if (keyEvent.assigned && keyEvent(this, event)) 1194 return true; // processed by external handler 1195 if (event.action == KeyAction.KeyDown) { 1196 //Log.d("Find key action for key = ", event.keyCode, " flags=", event.flags); 1197 Action action = findKeyAction(event.keyCode, event.flags); // & (KeyFlag.Shift | KeyFlag.Alt | KeyFlag.Control | KeyFlag.Menu) 1198 if (action !is null) { 1199 //Log.d("Action found: ", action.id, " ", action.labelValue.id); 1200 // update action state 1201 if ((action.stateUpdateFlag & ActionStateUpdateFlag.inAccelerator) && updateActionState(action, true) && action is _action) 1202 handleActionStateChanged(); 1203 1204 //run only enabled actions 1205 if (action.state.enabled) 1206 return dispatchAction(action); 1207 } 1208 } 1209 // handle focus navigation using keys 1210 if (focused && handleMoveFocusUsingKeys(event)) 1211 return true; 1212 if (canClick) { 1213 // support onClick event initiated by Space or Return keys 1214 if (event.action == KeyAction.KeyDown) { 1215 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) { 1216 setState(State.Pressed); 1217 return true; 1218 } 1219 } 1220 if (event.action == KeyAction.KeyUp) { 1221 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) { 1222 resetState(State.Pressed); 1223 handleClick(); 1224 return true; 1225 } 1226 } 1227 } 1228 return false; 1229 } 1230 1231 /// handle custom event 1232 bool onEvent(CustomEvent event) { 1233 RunnableEvent runnable = cast(RunnableEvent)event; 1234 if (runnable) { 1235 // handle runnable 1236 runnable.run(); 1237 return true; 1238 } 1239 // override to handle more events 1240 return false; 1241 } 1242 1243 /// 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) 1244 void executeInUiThread(void delegate() runnable) { 1245 if (!window) 1246 return; 1247 RunnableEvent event = new RunnableEvent(CUSTOM_RUNNABLE, this, runnable); 1248 window.postEvent(event); 1249 } 1250 1251 /// process mouse event; return true if event is processed by widget. 1252 bool onMouseEvent(MouseEvent event) { 1253 if (mouseEvent.assigned && mouseEvent(this, event)) 1254 return true; // processed by external handler 1255 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); 1256 // support onClick 1257 if (canClick) { 1258 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { 1259 setState(State.Pressed); 1260 if (canFocus) 1261 setFocus(); 1262 return true; 1263 } 1264 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { 1265 resetState(State.Pressed); 1266 handleClick(); 1267 return true; 1268 } 1269 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 1270 resetState(State.Pressed); 1271 resetState(State.Hovered); 1272 return true; 1273 } 1274 if (event.action == MouseAction.FocusIn) { 1275 setState(State.Pressed); 1276 return true; 1277 } 1278 } 1279 if (event.action == MouseAction.Move && !event.hasModifiers && hasTooltip) { 1280 scheduleTooltip(200); 1281 } 1282 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Right) { 1283 if (canShowPopupMenu(event.x, event.y)) { 1284 showPopupMenu(event.x, event.y); 1285 return true; 1286 } 1287 } 1288 if (canFocus && event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { 1289 setFocus(); 1290 return true; 1291 } 1292 if (trackHover) { 1293 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 1294 if ((state & State.Hovered)) { 1295 debug(mouse) Log.d("Hover off ", id); 1296 resetState(State.Hovered); 1297 } 1298 return true; 1299 } 1300 if (event.action == MouseAction.Move) { 1301 if (!(state & State.Hovered)) { 1302 debug(mouse) Log.d("Hover ", id); 1303 if (!TOUCH_MODE) 1304 setState(State.Hovered); 1305 } 1306 return true; 1307 } 1308 if (event.action == MouseAction.Leave) { 1309 debug(mouse) Log.d("Leave ", id); 1310 resetState(State.Hovered); 1311 return true; 1312 } 1313 } 1314 return false; 1315 } 1316 1317 // ======================================================= 1318 // Signals 1319 1320 /// on click event listener (bool delegate(Widget)) 1321 Signal!OnClickHandler click; 1322 1323 /// checked state change event listener (bool delegate(Widget, bool)) 1324 Signal!OnCheckHandler checkChange; 1325 1326 /// focus state change event listener (bool delegate(Widget, bool)) 1327 Signal!OnFocusHandler focusChange; 1328 1329 /// key event listener (bool delegate(Widget, KeyEvent)) - return true if event is processed by handler 1330 Signal!OnKeyHandler keyEvent; 1331 1332 /// action by key lookup handler 1333 Listener!OnKeyActionHandler keyToAction; 1334 1335 /// action handlers 1336 Signal!OnActionHandler onAction; 1337 1338 /// mouse event listener (bool delegate(Widget, MouseEvent)) - return true if event is processed by handler 1339 Signal!OnMouseHandler mouseEvent; 1340 1341 1342 // Signal utils 1343 1344 /// helper function to add onCheckChangeListener in method chain 1345 Widget addOnClickListener(bool delegate(Widget) listener) { 1346 click.connect(listener); 1347 return this; 1348 } 1349 1350 /// helper function to add onCheckChangeListener in method chain 1351 Widget addOnCheckChangeListener(bool delegate(Widget, bool) listener) { 1352 checkChange.connect(listener); 1353 return this; 1354 } 1355 1356 /// helper function to add onFocusChangeListener in method chain 1357 Widget addOnFocusChangeListener(bool delegate(Widget, bool) listener) { 1358 focusChange.connect(listener); 1359 return this; 1360 } 1361 1362 // ======================================================= 1363 // Layout and measurement methods 1364 1365 /// request relayout of widget and its children 1366 void requestLayout() { 1367 _needLayout = true; 1368 } 1369 /// request redraw 1370 void invalidate() { 1371 _needDraw = true; 1372 } 1373 1374 /// helper function for implement measure() when widget's content dimensions are known 1375 protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) { 1376 if (visibility == Visibility.Gone) { 1377 _measuredWidth = _measuredHeight = 0; 1378 return; 1379 } 1380 Rect m = margins; 1381 Rect p = padding; 1382 // summarize margins, padding, and content size 1383 int dx = m.left + m.right + p.left + p.right + contentWidth; 1384 int dy = m.top + m.bottom + p.top + p.bottom + contentHeight; 1385 // check for fixed size set in layoutWidth, layoutHeight 1386 int lh = layoutHeight; 1387 int lw = layoutWidth; 1388 // constant value support 1389 if (!(isPercentSize(lh) || isSpecialSize(lh))) 1390 dy = lh.toPixels(); 1391 if (!(isPercentSize(lw) || isSpecialSize(lw))) 1392 dx = lw.toPixels(); 1393 // apply min/max width and height constraints 1394 int minw = minWidth; 1395 int maxw = maxWidth; 1396 int minh = minHeight; 1397 int maxh = maxHeight; 1398 if (minw != SIZE_UNSPECIFIED && dx < minw) 1399 dx = minw; 1400 if (minh != SIZE_UNSPECIFIED && dy < minh) 1401 dy = minh; 1402 if (maxw != SIZE_UNSPECIFIED && dx > maxw) 1403 dx = maxw; 1404 if (maxh != SIZE_UNSPECIFIED && dy > maxh) 1405 dy = maxh; 1406 // apply FILL_PARENT 1407 //if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT) 1408 // dx = parentWidth; 1409 //if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT) 1410 // dy = parentHeight; 1411 // apply max parent size constraint 1412 if (parentWidth != SIZE_UNSPECIFIED && dx > parentWidth) 1413 dx = parentWidth; 1414 if (parentHeight != SIZE_UNSPECIFIED && dy > parentHeight) 1415 dy = parentHeight; 1416 _measuredWidth = dx; 1417 _measuredHeight = dy; 1418 } 1419 1420 /** 1421 Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 1422 1423 */ 1424 void measure(int parentWidth, int parentHeight) { 1425 measuredContent(parentWidth, parentHeight, 0, 0); 1426 } 1427 1428 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 1429 void layout(Rect rc) { 1430 if (visibility == Visibility.Gone) { 1431 return; 1432 } 1433 _pos = rc; 1434 _needLayout = false; 1435 } 1436 1437 /// draws focus rectangle, if enabled in styles 1438 void drawFocusRect(DrawBuf buf, Rect rc) { 1439 const uint[] colors = focusRectColors; 1440 if (colors) { 1441 buf.drawFocusRect(rc, colors); 1442 } 1443 } 1444 1445 /// Draw widget at its position to buffer 1446 void onDraw(DrawBuf buf) { 1447 if (visibility != Visibility.Visible) 1448 return; 1449 Rect rc = _pos; 1450 applyMargins(rc); 1451 auto saver = ClipRectSaver(buf, rc, alpha); 1452 DrawableRef bg = backgroundDrawable; 1453 if (!bg.isNull) { 1454 bg.drawTo(buf, rc, state); 1455 } 1456 applyPadding(rc); 1457 if (state & State.Focused) { 1458 rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING); 1459 drawFocusRect(buf, rc); 1460 } 1461 _needDraw = false; 1462 } 1463 1464 /// Helper function: applies margins to rectangle 1465 void applyMargins(ref Rect rc) { 1466 Rect m = margins; 1467 rc.left += m.left; 1468 rc.top += m.top; 1469 rc.bottom -= m.bottom; 1470 rc.right -= m.right; 1471 } 1472 /// Helper function: applies padding to rectangle 1473 void applyPadding(ref Rect rc) { 1474 Rect m = padding; 1475 rc.left += m.left; 1476 rc.top += m.top; 1477 rc.bottom -= m.bottom; 1478 rc.right -= m.right; 1479 } 1480 /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc. 1481 static void applyAlign(ref Rect rc, Point sz, Align ha, Align va) { 1482 if (va == Align.Bottom) { 1483 rc.top = rc.bottom - sz.y; 1484 } else if (va == Align.VCenter) { 1485 int dy = (rc.height - sz.y) / 2; 1486 rc.top += dy; 1487 rc.bottom = rc.top + sz.y; 1488 } else { 1489 rc.bottom = rc.top + sz.y; 1490 } 1491 if (ha == Align.Right) { 1492 rc.left = rc.right - sz.x; 1493 } else if (ha == Align.HCenter) { 1494 int dx = (rc.width - sz.x) / 2; 1495 rc.left += dx; 1496 rc.right = rc.left + sz.x; 1497 } else { 1498 rc.right = rc.left + sz.x; 1499 } 1500 } 1501 /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc. 1502 void applyAlign(ref Rect rc, Point sz) { 1503 Align va = valign; 1504 Align ha = halign; 1505 applyAlign(rc, sz, ha, va); 1506 } 1507 1508 // =========================================================== 1509 // popup menu support 1510 override bool onMenuItemAction(const Action action) { 1511 return dispatchAction(action); 1512 } 1513 1514 protected MenuItem _popupMenu; 1515 @property MenuItem popupMenu() { return _popupMenu; } 1516 @property Widget popupMenu(MenuItem popupMenu) { 1517 _popupMenu = popupMenu; 1518 return this; 1519 } 1520 1521 /// returns true if widget can show popup menu (e.g. by mouse right click at point x,y) 1522 bool canShowPopupMenu(int x, int y) { 1523 if (_popupMenu is null) 1524 return false; 1525 if (_popupMenu.openingSubmenu.assigned) 1526 if (!_popupMenu.openingSubmenu(_popupMenu)) 1527 return false; 1528 return true; 1529 } 1530 /// shows popup menu at (x,y) 1531 void showPopupMenu(int x, int y) { 1532 /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler 1533 if (_popupMenu.openingSubmenu.assigned) 1534 if (!_popupMenu.openingSubmenu(_popupMenu)) 1535 return; 1536 _popupMenu.updateActionState(this); 1537 import dlangui.widgets.popup; 1538 PopupMenu popupMenu = new PopupMenu(_popupMenu); 1539 popupMenu.menuItemAction = this; 1540 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y); 1541 popup.flags = PopupFlags.CloseOnClickOutside; 1542 } 1543 /// override to change popup menu items state 1544 bool isActionEnabled(const Action action) { 1545 return true; 1546 } 1547 1548 // =========================================================== 1549 // Widget hierarhy methods 1550 1551 /// returns number of children of this widget 1552 @property int childCount() const { return 0; } 1553 /// returns child by index 1554 inout(Widget) child(int index) inout { return null; } 1555 /// adds child, returns added item 1556 Widget addChild(Widget item) { assert(false, "addChild: children not suported for this widget type"); } 1557 /// adds child, returns added item 1558 Widget addChildren(Widget[] items) { 1559 foreach(item; items) { 1560 addChild(item); 1561 } 1562 return this; 1563 } 1564 /// inserts child at given index, returns inserted item 1565 Widget insertChild(Widget item, int index) {assert(false, "insertChild: children not suported for this widget type"); } 1566 /// removes child, returns removed item 1567 Widget removeChild(int index) { assert(false, "removeChild: children not suported for this widget type"); } 1568 /// removes child by ID, returns removed item 1569 Widget removeChild(string id) { assert(false, "removeChild: children not suported for this widget type"); } 1570 /// removes child, returns removed item 1571 Widget removeChild(Widget child) { assert(false, "removeChild: children not suported for this widget type"); } 1572 /// returns index of widget in child list, -1 if passed widget is not a child of this widget 1573 int childIndex(Widget item) { return -1; } 1574 1575 1576 /// 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). 1577 bool isChild(Widget item, bool deepSearch = true) { 1578 if (deepSearch) { 1579 // this widget or some widget inside children tree 1580 if (item is this) 1581 return true; 1582 for (int i = 0; i < childCount; i++) { 1583 if (child(i).isChild(item)) 1584 return true; 1585 } 1586 } else { 1587 // only one of children 1588 for (int i = 0; i < childCount; i++) { 1589 if (item is child(i)) 1590 return true; 1591 } 1592 } 1593 return false; 1594 } 1595 1596 /// find child of specified type T by id, returns null if not found or cannot be converted to type T 1597 T childById(T = typeof(this))(string id, bool deepSearch = true) { 1598 if (deepSearch) { 1599 // search everywhere inside child tree 1600 if (compareId(id)) { 1601 T found = cast(T)this; 1602 if (found) 1603 return found; 1604 } 1605 // lookup children 1606 for (int i = childCount - 1; i >= 0; i--) { 1607 Widget res = child(i).childById(id); 1608 if (res !is null) { 1609 T found = cast(T)res; 1610 if (found) 1611 return found; 1612 } 1613 } 1614 } else { 1615 // search only across children of this widget 1616 for (int i = childCount - 1; i >= 0; i--) { 1617 Widget w = child(i); 1618 if (id.equal(w.id)) { 1619 T found = cast(T)w; 1620 if (found) 1621 return found; 1622 } 1623 } 1624 } 1625 // not found 1626 return null; 1627 } 1628 1629 /// returns parent widget, null for top level widget 1630 @property Widget parent() const { return _parent ? cast(Widget)_parent : null; } 1631 /// sets parent for widget 1632 @property Widget parent(Widget parent) { _parent = parent; return this; } 1633 /// returns window (if widget or its parent is attached to window) 1634 @property Window window() const { 1635 Widget p = cast(Widget)this; 1636 while (p !is null) { 1637 if (p._window !is null) 1638 return cast(Window)p._window; 1639 p = p.parent; 1640 } 1641 return null; 1642 } 1643 /// sets window (to be used for top level widget from Window implementation). TODO: hide it from API? 1644 @property void window(Window window) { 1645 _window = window; 1646 } 1647 1648 void removeAllChildren(bool destroyObj = true) { 1649 // override 1650 } 1651 1652 /// set string property value, for ML loaders 1653 bool setStringProperty(string name, string value) { 1654 mixin(generatePropertySetters("id", "styleId", "backgroundImageId", "backgroundColor", "textColor", "fontFace")); 1655 if (name.equal("text")) { 1656 text = UIString.fromId(value); 1657 return true; 1658 } 1659 if (name.equal("tooltipText")) { 1660 tooltipText = UIString.fromId(value); 1661 return true; 1662 } 1663 return false; 1664 } 1665 1666 /// set string property value, for ML loaders 1667 bool setDstringProperty(string name, dstring value) { 1668 if (name.equal("text")) { 1669 text = UIString.fromRaw(value); 1670 return true; 1671 } 1672 if (name.equal("tooltipText")) { 1673 tooltipText = UIString.fromRaw(value); 1674 return true; 1675 } 1676 return false; 1677 } 1678 1679 /// set string property value, for ML loaders 1680 bool setUistringProperty(string name, UIString value) { 1681 if (name.equal("text")) { 1682 text = value; 1683 return true; 1684 } 1685 if (name.equal("tooltipText")) { 1686 tooltipText = value; 1687 return true; 1688 } 1689 return false; 1690 } 1691 1692 /// StringListValue list values 1693 bool setStringListValueListProperty(string propName, StringListValue[] values) { 1694 return false; 1695 } 1696 1697 /// UIString list values 1698 bool setUIStringListProperty(string propName, UIString[] values) { 1699 return false; 1700 } 1701 1702 /// set string property value, for ML loaders 1703 bool setBoolProperty(string name, bool value) { 1704 mixin(generatePropertySetters("enabled", "clickable", "checkable", "focusable", "checked", "fontItalic")); 1705 return false; 1706 } 1707 1708 /// set double property value, for ML loaders 1709 bool setDoubleProperty(string name, double value) { 1710 if (name.equal("alpha")) { 1711 int n = cast(int)(value * 255); 1712 return setIntProperty(name, n); 1713 } 1714 return false; 1715 } 1716 1717 /// set int property value, for ML loaders 1718 bool setIntProperty(string name, int value) { 1719 if (name.equal("alpha")) { 1720 if (value < 0) 1721 value = 0; 1722 else if (value > 255) 1723 value = 255; 1724 alpha = cast(ushort)value; 1725 return true; 1726 } 1727 mixin(generatePropertySetters("minWidth", "maxWidth", "minHeight", "maxHeight", "layoutWidth", "layoutHeight", "layoutWeight", "textColor", "backgroundColor", "fontSize", "fontWeight")); 1728 if (name.equal("margins")) { // use same value for all sides 1729 margins = Rect(value, value, value, value); 1730 return true; 1731 } 1732 if (name.equal("alignment")) { 1733 alignment = cast(Align)value; 1734 return true; 1735 } 1736 if (name.equal("padding")) { // use same value for all sides 1737 padding = Rect(value, value, value, value); 1738 return true; 1739 } 1740 return false; 1741 } 1742 1743 /// set Rect property value, for ML loaders 1744 bool setRectProperty(string name, Rect value) { 1745 mixin(generatePropertySetters("margins", "padding")); 1746 return false; 1747 } 1748 } 1749 1750 /** Widget list holder. */ 1751 alias WidgetList = ObjectList!Widget; 1752 1753 /** Base class for widgets which have children. */ 1754 class WidgetGroup : Widget { 1755 1756 /// empty parameter list constructor - for usage by factory 1757 this() { 1758 this(null); 1759 } 1760 /// create with ID parameter 1761 this(string ID) { 1762 super(ID); 1763 } 1764 1765 protected WidgetList _children; 1766 1767 /// returns number of children of this widget 1768 @property override int childCount() const { return _children.count; } 1769 /// returns child by index 1770 override inout(Widget) child(int index) inout { return _children.get(index); } 1771 /// adds child, returns added item 1772 override Widget addChild(Widget item) { return _children.add(item).parent(this); } 1773 /// inserts child at given index, returns inserted item 1774 override Widget insertChild(Widget item, int index) { return _children.insert(item,index).parent(this); } 1775 /// removes child, returns removed item 1776 override Widget removeChild(int index) { 1777 Widget res = _children.remove(index); 1778 if (res !is null) 1779 res.parent = null; 1780 return res; 1781 } 1782 /// removes child by ID, returns removed item 1783 override Widget removeChild(string ID) { 1784 Widget res = null; 1785 int index = _children.indexOf(ID); 1786 if (index < 0) 1787 return null; 1788 res = _children.remove(index); 1789 if (res !is null) 1790 res.parent = null; 1791 return res; 1792 } 1793 /// removes child, returns removed item 1794 override Widget removeChild(Widget child) { 1795 Widget res = null; 1796 int index = _children.indexOf(child); 1797 if (index < 0) 1798 return null; 1799 res = _children.remove(index); 1800 if (res !is null) 1801 res.parent = null; 1802 return res; 1803 } 1804 /// returns index of widget in child list, -1 if passed widget is not a child of this widget 1805 override int childIndex(Widget item) { return _children.indexOf(item); } 1806 1807 override void removeAllChildren(bool destroyObj = true) { 1808 _children.clear(destroyObj); 1809 } 1810 1811 /// replace child with other child 1812 void replaceChild(Widget newChild, Widget oldChild) { 1813 _children.replace(newChild, oldChild); 1814 } 1815 1816 } 1817 1818 /** WidgetGroup with default drawing of children (just draw all children) */ 1819 class WidgetGroupDefaultDrawing : WidgetGroup { 1820 /// empty parameter list constructor - for usage by factory 1821 this() { 1822 this(null); 1823 } 1824 /// create with ID parameter 1825 this(string ID) { 1826 super(ID); 1827 } 1828 /// Draw widget at its position to buffer 1829 override void onDraw(DrawBuf buf) { 1830 if (visibility != Visibility.Visible) 1831 return; 1832 super.onDraw(buf); 1833 Rect rc = _pos; 1834 applyMargins(rc); 1835 applyPadding(rc); 1836 auto saver = ClipRectSaver(buf, rc); 1837 for (int i = 0; i < _children.count; i++) { 1838 Widget item = _children.get(i); 1839 item.onDraw(buf); 1840 } 1841 } 1842 } 1843 1844 /// helper for locating items in list, tree, table or other controls by typing their name 1845 struct TextTypingShortcutHelper { 1846 int timeoutMillis = 800; // expiration time for entered text; after timeout collected text will be cleared 1847 private long _lastUpdateTimeStamp; 1848 private dchar[] _text; 1849 /// cancel text collection (next typed text will be collected from scratch) 1850 void cancel() { 1851 _text.length = 0; 1852 _lastUpdateTimeStamp = 0; 1853 } 1854 /// returns collected text string - use it for lookup 1855 @property dstring text() { return _text.dup; } 1856 /// pass key event here; returns true if search text is updated and you can move selection using it 1857 bool onKeyEvent(KeyEvent event) { 1858 long ts = currentTimeMillis; 1859 if (_lastUpdateTimeStamp && ts - _lastUpdateTimeStamp > timeoutMillis) 1860 cancel(); 1861 if (event.action == KeyAction.Text) { 1862 _text ~= event.text; 1863 _lastUpdateTimeStamp = ts; 1864 return _text.length > 0; 1865 } 1866 if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) { 1867 switch (event.keyCode) with (KeyCode) { 1868 case LEFT: 1869 case RIGHT: 1870 case UP: 1871 case DOWN: 1872 case HOME: 1873 case END: 1874 case TAB: 1875 case PAGEUP: 1876 case PAGEDOWN: 1877 case BACK: 1878 cancel(); 1879 break; 1880 default: 1881 break; 1882 } 1883 } 1884 return false; 1885 } 1886 1887 /// cancel text typing on some mouse events, if necessary 1888 void onMouseEvent(MouseEvent event) { 1889 if (event.action == MouseAction.ButtonUp || event.action == MouseAction.ButtonDown) 1890 cancel(); 1891 } 1892 } 1893 1894 1895 enum ONE_SECOND = 10_000_000L; 1896 1897 /// Helper to handle animation progress 1898 struct AnimationHelper { 1899 private long _timeElapsed; 1900 private long _maxInterval; 1901 private int _maxProgress; 1902 1903 /// start new animation interval 1904 void start(long maxInterval, int maxProgress) { 1905 _timeElapsed = 0; 1906 _maxInterval = maxInterval; 1907 _maxProgress = maxProgress; 1908 assert(_maxInterval > 0); 1909 assert(_maxProgress > 0); 1910 } 1911 /// Adds elapsed time; returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress 1912 int animate(long time) { 1913 _timeElapsed += time; 1914 return progress(); 1915 } 1916 /// restart with same max interval and progress 1917 void restart() { 1918 if (!_maxInterval) { 1919 _maxInterval = ONE_SECOND; 1920 } 1921 _timeElapsed = 0; 1922 } 1923 /// returns time elapsed since start 1924 @property long elapsed() { 1925 return _timeElapsed; 1926 } 1927 /// get current time interval 1928 @property long interval() { 1929 return _maxInterval; 1930 } 1931 /// override current time interval, retaining the same progress % 1932 @property void interval(long newInterval) { 1933 int p = getProgress(10000); 1934 _maxInterval = newInterval; 1935 _timeElapsed = p * newInterval / 10000; 1936 } 1937 /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress 1938 @property int progress() { 1939 return getProgress(_maxProgress); 1940 } 1941 /// Returns animation progress in interval 0..maxProgress while timeElapsed is between 0 and maxInterval; when interval exceeded, progress is maxProgress 1942 int getProgress(int maxProgress) { 1943 if (finished) 1944 return maxProgress; 1945 if (_timeElapsed <= 0) 1946 return 0; 1947 return cast(int)(_timeElapsed * maxProgress / _maxInterval); 1948 } 1949 /// Returns true if animation is finished 1950 @property bool finished() { 1951 return _timeElapsed >= _maxInterval; 1952 } 1953 } 1954 1955 1956 /// mixin this to widget class to support tooltips based on widget's action label 1957 mixin template ActionTooltipSupport() { 1958 /// returns true if widget has tooltip to show 1959 override @property bool hasTooltip() { 1960 if (!_action || _action.labelValue.empty) 1961 return false; 1962 return true; 1963 } 1964 /// 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 1965 override Widget createTooltip(int mouseX, int mouseY, ref uint alignment, ref int x, ref int y) { 1966 Widget res = new TextWidget("tooltip", _action.tooltipText); 1967 res.styleId = STYLE_TOOLTIP; 1968 return res; 1969 } 1970 } 1971 1972 /// use in mixin to set this object property with name propName with value of variable value if variable name matches propName 1973 string generatePropertySetter(string propName) { 1974 return " if (name.equal(\"" ~ propName ~ "\")) { \n" ~ 1975 " " ~ propName ~ " = value;\n" ~ 1976 " return true;\n" ~ 1977 " }\n"; 1978 } 1979 1980 /// use in mixin to set this object properties with names from parameter list with value of variable value if variable name matches propName 1981 string generatePropertySetters(string[] propNames...) { 1982 string res; 1983 foreach(propName; propNames) 1984 res ~= generatePropertySetter(propName); 1985 return res; 1986 } 1987 1988 /// 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 1989 string generatePropertySettersMethodOverride(string methodName, string typeName, string[] propNames...) { 1990 string res = " override bool " ~ methodName ~ "(string name, " ~ typeName ~ " value) {\n" ~ 1991 " import std.algorithm : equal;\n"; 1992 foreach(propName; propNames) 1993 res ~= generatePropertySetter(propName); 1994 res ~= " return super." ~ methodName ~ "(name, value);\n" ~ 1995 " }\n"; 1996 return res; 1997 } 1998 1999 2000 __gshared bool TOUCH_MODE = false;