1 /++ 2 Contains all the classes that make up qui. 3 +/ 4 module qui.qui; 5 6 import utils.misc; 7 import utils.ds; 8 9 import std.datetime.stopwatch; 10 import std.conv : to; 11 import std.process; 12 13 import qui.termwrap; 14 import qui.utils; 15 16 /// default foreground color, white 17 enum Color DEFAULT_FG = Color.DEFAULT; 18 /// default background color, black 19 enum Color DEFAULT_BG = Color.DEFAULT; 20 /// default background color of overflowing layouts, red 21 enum Color DEFAULT_OVERFLOW_BG = Color.red; 22 /// default active widget cycling key, tab 23 enum dchar WIDGET_CYCLE_KEY = '\t'; 24 25 /// Colors 26 alias Color = qui.termwrap.Color; 27 /// Availabe Keys (keyboard) for input 28 alias Key = qui.termwrap.Event.Keyboard.Key; 29 /// Mouse Event 30 alias MouseEvent = Event.Mouse; 31 /// Keyboard Event 32 alias KeyboardEvent = Event.Keyboard; 33 34 /// MouseEvent function. Return true to drop event 35 alias MouseEventFuction = bool delegate(QWidget, MouseEvent); 36 /// KeyboardEvent function. Return true to drop event 37 alias KeyboardEventFunction = bool delegate(QWidget, KeyboardEvent, bool); 38 /// ResizeEvent function. Return true to drop event 39 alias ResizeEventFunction = bool delegate(QWidget); 40 /// ScrollEvent function. Return true to drop event 41 alias ScrollEventFunction = bool delegate(QWidget); 42 /// ActivateEvent function. Return true to drop event 43 alias ActivateEventFunction = bool delegate(QWidget, bool); 44 /// TimerEvent function. Return true to drop event 45 alias TimerEventFunction = bool delegate(QWidget, uint); 46 /// Init function. Return true to drop event 47 alias InitFunction = bool delegate(QWidget); 48 /// UpdateEvent function. Return true to drop event 49 alias UpdateEventFunction = bool delegate(QWidget); 50 51 /// mask of events subscribed 52 enum EventMask : uint{ 53 /// mouse clicks/presses. 54 /// This value matches `MouseEvent.State.Click` 55 MousePress = 1, 56 /// mouse releases 57 /// This value matches `MouseEvent.State.Release` 58 MouseRelease = 1 << 1, 59 /// mouse move/hover. 60 /// This value matches `MouseEvent.State.Hover` 61 MouseHover = 1 << 2, 62 /// key presses. 63 /// This value matches `KeyboardEvent.State.Pressed` 64 KeyboardPress = 1 << 3, 65 /// key releases. 66 /// This value matches `KeyboardEvent.State.Released` 67 KeyboardRelease = 1 << 4, 68 /// widget scrolling. 69 Scroll = 1 << 5, 70 /// widget resize. 71 Resize = 1 << 6, 72 /// widget activated/deactivated. 73 Activate = 1 << 7, 74 /// timer 75 Timer = 1 << 8, 76 /// initialize 77 Initialize = 1 << 9, 78 /// draw itself. 79 Update = 1 << 10, 80 /// All mouse events 81 MouseAll = MousePress | MouseRelease | MouseHover, 82 /// All keyboard events 83 KeyboardAll = KeyboardPress | KeyboardRelease, 84 /// All keyboard and mouse events 85 InputAll = MouseAll | KeyboardAll, 86 } 87 88 /// A cell on terminal 89 struct Cell{ 90 /// character 91 dchar c; 92 /// foreground color 93 Color fg; 94 /// background colors 95 Color bg; 96 /// if colors are same with another Cell 97 bool colorsSame(Cell b){ 98 return this.fg == b.fg && this.bg == b.fg; 99 } 100 } 101 102 /// Display buffer 103 struct Viewport{ 104 private: 105 Cell[] _buffer; 106 /// where cursor is right now 107 uint _seekX, _seekY; 108 /// size of buffer (from `SIZE - offset`) 109 uint _width, _height; 110 /// actual width of the full buffer 111 uint _actualWidth; 112 // these are subtracted, only when seek is set, not after 113 uint _offsetX, _offsetY; 114 115 /// reset 116 void _reset(){ 117 _buffer = []; 118 _seekX = 0; 119 _seekY = 0; 120 _width = 0; 121 _height = 0; 122 _actualWidth = 0; 123 _offsetX = 0; 124 _offsetY = 0; 125 } 126 127 /// seek position in _buffer calculated from _seekX & _seekY 128 @property int _seek(){ 129 return (_seekX - _offsetX) + ((_seekY - _offsetY) * _actualWidth); 130 } 131 /// set another Viewport so that it is a rectangular slice 132 /// of this 133 /// 134 /// Returns: true if done, false if not 135 void _getSlice(ref Viewport sub, int x, int y, uint width, uint height){ 136 sub._reset(); 137 sub._actualWidth = _actualWidth; 138 if (width == 0 || height == 0) 139 return; 140 x -= _offsetX; 141 y -= _offsetY; 142 if (x > cast(int)_width || y > cast(int)_height || 143 x + width <= 0 || y + height <= 0) 144 return; 145 if (x < 0){ 146 sub._offsetX = -x; 147 width += x; 148 x = 0; 149 } 150 if (y < 0){ 151 sub._offsetY = -y; 152 height += y; 153 y = 0; 154 } 155 if (width + x > _width) 156 width = _width - x; 157 if (height + y > _height) 158 height = _height - y; 159 immutable uint buffStart = x + (y * _actualWidth), 160 buffEnd = buffStart + ((height - 1) * _actualWidth) + width; 161 if (buffEnd > _buffer.length || buffStart >= buffEnd) 162 return; 163 sub._width = width; 164 sub._height = height; 165 sub._buffer = _buffer[buffStart .. buffEnd]; 166 } 167 public: 168 /// if x and y are at a position where writing can happen 169 bool isWritable(uint x, uint y){ 170 return x >= _offsetX && y >= _offsetY && 171 x < _width + _offsetX && y < _height + _offsetY; 172 } 173 /// move to a position. if x > width, moved to x=0 of next row 174 void moveTo(uint x, uint y){ 175 _seekX = x; 176 _seekY = y; 177 if (_seekX >= _width){ 178 _seekX = 0; 179 _seekY ++; 180 } 181 } 182 /// Writes a character at current position and move ahead 183 /// 184 /// Returns: false if outside writing area 185 bool write(dchar c, Color fg, Color bg){ 186 if (_seekX < _offsetX || _seekY < _offsetY){ 187 _seekX ++; 188 if (_seekX >= _width){ 189 _seekX = 0; 190 _seekY ++; 191 } 192 return false; 193 } 194 if (_seekX >= _width && _seekY >= _height) 195 return false; 196 if (_buffer.length > _seek) 197 _buffer[_seek] = Cell(c, fg, bg); 198 _seekX ++; 199 if (_seekX >= _width){ 200 _seekX = 0; 201 _seekY ++; 202 } 203 return true; 204 } 205 /// Writes a string. 206 /// 207 /// Returns: number of characters written 208 uint write(dstring s, Color fg, Color bg){ 209 uint r; 210 foreach (c; s){ 211 if (!write(c, fg, bg)) 212 break; 213 r ++; 214 } 215 return r; 216 } 217 /// Fills line, starting from current coordinates, 218 /// with maximum `max` number of chars, if `max>0` 219 /// 220 /// Returns: number of characters written 221 uint fillLine(dchar c, Color fg, Color bg, uint max=0){ 222 uint r = 0; 223 const uint currentY = _seekY; 224 bool status = true; 225 while (status && (max == 0 || r < max) && _seekY == currentY){ 226 status = write(c, fg, bg); 227 r ++; 228 } 229 return r; 230 } 231 } 232 233 /// Base class for all widgets, including layouts and QTerminal 234 /// 235 /// Use this as parent-class for new widgets 236 abstract class QWidget{ 237 private: 238 /// position of this widget, relative to parent 239 uint _posX, _posY; 240 /// width of widget 241 uint _width; 242 /// height of widget 243 uint _height; 244 /// scroll 245 uint _scrollX, _scrollY; 246 /// viewport 247 Viewport _view; 248 /// if this widget is the active widget 249 bool _isActive = false; 250 /// whether this widget is requesting update 251 bool _requestingUpdate = true; 252 /// Whether to call resize before next update 253 bool _requestingResize = false; 254 /// the parent widget 255 QWidget _parent = null; 256 /// if it can make parent change scrolling 257 bool _canReqScroll = false; 258 /// if this widget is itself a scrollable container 259 bool _isScrollableContainer = false; 260 /// Events this widget is subscribed to, see `EventMask` 261 uint _eventSub = 0; 262 /// whether this widget should be drawn or not. 263 bool _show = true; 264 /// specifies ratio of height or width 265 uint _sizeRatio = 1; 266 267 /// custom onInit event 268 InitFunction _customInitEvent; 269 /// custom mouse event 270 MouseEventFuction _customMouseEvent; 271 /// custom keyboard event 272 KeyboardEventFunction _customKeyboardEvent; 273 /// custom resize event 274 ResizeEventFunction _customResizeEvent; 275 /// custom rescroll event 276 ScrollEventFunction _customScrollEvent; 277 /// custom onActivate event, 278 ActivateEventFunction _customActivateEvent; 279 /// custom onTimer event 280 TimerEventFunction _customTimerEvent; 281 /// custom upedateEvent 282 UpdateEventFunction _customUpdateEvent; 283 284 /// Called when it needs to request an update. 285 void _requestUpdate(){ 286 if (_requestingUpdate || !(_eventSub & EventMask.Update)) 287 return; 288 _requestingUpdate = true; 289 if (_parent) 290 _parent.requestUpdate(); 291 } 292 /// Called to request this widget to resize at next update 293 void _requestResize(){ 294 if (_requestingResize) 295 return; 296 _requestingResize = true; 297 if (_parent) 298 _parent.requestResize(); 299 } 300 /// Called to request cursor to be positioned at x,y 301 /// Will do nothing if not active widget 302 void _requestCursorPos(int x, int y){ 303 if (_isActive && _parent) 304 _parent.requestCursorPos( 305 x < 0 ? x : _posX + x - _view._offsetX, 306 y < 0 ? y : _posY + y - _view._offsetY); 307 } 308 /// Called to request _scrollX to be adjusted 309 /// Returns: true if _scrollX was modified 310 bool _requestScrollX(uint x){ 311 if (_canReqScroll && _parent && _view._width < _width && 312 x < _width - _view._width) 313 return _parent.requestScrollX(x + _posX); 314 return false; 315 } 316 /// Called to request _scrollY to be adjusted 317 /// Returns: true if _scrollY was modified 318 bool _requestScrollY(uint y){ 319 if (_canReqScroll && _parent && _view._height < _height && 320 y < _height - _view._height) 321 return _parent.requestScrollY(y + _posY); 322 return false; 323 } 324 325 /// called by parent for initialize event 326 bool _initializeCall(){ 327 if (!(_eventSub & EventMask.Initialize)) 328 return false; 329 if (_customInitEvent && _customInitEvent(this)) 330 return true; 331 return this.initialize(); 332 } 333 /// called by parent for mouseEvent 334 bool _mouseEventCall(MouseEvent mouse){ 335 if (!(_eventSub & mouse.state)) 336 return false; 337 // mouse input comes relative to visible area 338 mouse.x = (mouse.x - cast(int)this._posX) + _view._offsetX; 339 mouse.y = (mouse.y - cast(int)this._posY) + _view._offsetY; 340 if (_customMouseEvent && _customMouseEvent(this, mouse)) 341 return true; 342 return this.mouseEvent(mouse); 343 } 344 /// called by parent for keyboardEvent 345 bool _keyboardEventCall(KeyboardEvent key, bool cycle){ 346 if (!(_eventSub & key.state)) 347 return false; 348 if (_customKeyboardEvent && _customKeyboardEvent(this, key, cycle)) 349 return true; 350 return this.keyboardEvent(key, cycle); 351 } 352 /// called by parent for resizeEvent 353 bool _resizeEventCall(){ 354 _requestingResize = false; 355 _requestUpdate(); 356 if (!(_eventSub & EventMask.Resize)) 357 return false; 358 if (_customResizeEvent && _customResizeEvent(this)) 359 return true; 360 return this.resizeEvent(); 361 } 362 /// called by parent for scrollEvent 363 bool _scrollEventCall(){ 364 _requestUpdate(); 365 if (!(_eventSub & EventMask.Scroll)) 366 return false; 367 if (_customScrollEvent && _customScrollEvent(this)) 368 return true; 369 return this.scrollEvent(); 370 } 371 /// called by parent for activateEvent 372 bool _activateEventCall(bool isActive){ 373 if (!(_eventSub & EventMask.Activate)) 374 return false; 375 if (_isActive == isActive) 376 return false; 377 _isActive = isActive; 378 if (_customActivateEvent && _customActivateEvent(this, isActive)) 379 return true; 380 return this.activateEvent(isActive); 381 } 382 /// called by parent for timerEvent 383 bool _timerEventCall(uint msecs){ 384 if (!(_eventSub & EventMask.Timer)) 385 return false; 386 if (_customTimerEvent && _customTimerEvent(this, msecs)) 387 return true; 388 return timerEvent(msecs); 389 } 390 /// called by parent for updateEvent 391 bool _updateEventCall(){ 392 if (!_requestingUpdate || !(_eventSub & EventMask.Update)) 393 return false; 394 _requestingUpdate = false; 395 _view.moveTo(_view._offsetX,_view._offsetY); 396 if (_customUpdateEvent && _customUpdateEvent(this)) 397 return true; 398 return this.updateEvent(); 399 } 400 protected: 401 /// minimum width 402 uint _minWidth; 403 /// maximum width 404 uint _maxWidth; 405 /// minimum height 406 uint _minHeight; 407 /// maximum height 408 uint _maxHeight; 409 410 /// viewport coordinates. (drawable area for widget) 411 final @property uint viewportX(){ 412 return _view._offsetX; 413 } 414 /// ditto 415 final @property uint viewportY(){ 416 return _view._offsetY; 417 } 418 /// viewport size. (drawable area for widget) 419 final @property uint viewportWidth(){ 420 return _view._width; 421 } 422 /// ditto 423 final @property uint viewportHeight(){ 424 return _view._height; 425 } 426 427 /// If a coordinate is within writing area, 428 /// and writing area actually exists 429 final bool isWritable(uint x, uint y){ 430 return _view.isWritable(x,y); 431 } 432 433 /// move seek for next write to terminal. 434 /// can only write in between: 435 /// `(_viewX .. _viewX + _viewWidth, 436 /// _viewY .. _viewX + _viewHeight)` 437 final void moveTo(uint newX, uint newY){ 438 _view.moveTo(newX, newY); 439 } 440 /// writes a character on terminal 441 /// 442 /// Returns: true if done, false if outside writing area 443 final bool write(dchar c, Color fg, Color bg){ 444 return _view.write(c, fg, bg); 445 } 446 /// writes a string to terminal. 447 /// if it does not fit in one line, it is wrapped 448 /// 449 /// Returns: number of characters written 450 final uint write(dstring s, Color fg, Color bg){ 451 return _view.write(s, fg, bg); 452 } 453 /// fill current line with a character. 454 /// `max` is ignored if `max==0` 455 /// 456 /// Returns: number of cells written 457 final uint fillLine(dchar c, Color fg, Color bg, uint max = 0){ 458 return _view.fillLine(c, fg, bg, max); 459 } 460 461 /// activate the passed widget if this is the correct widget 462 /// 463 /// Returns: if it was activated or not 464 bool searchAndActivateWidget(QWidget target) { 465 if (this == target) { 466 this._isActive = true; 467 this._activateEventCall(true); 468 return true; 469 } 470 return false; 471 } 472 473 /// called by itself, to update events subscribed to 474 final void eventSubscribe(uint newSub){ 475 _eventSub = newSub; 476 if (_parent) 477 _parent.eventSubscribe(); 478 } 479 /// called by children when they want to subscribe to events 480 void eventSubscribe(){} 481 482 /// to set cursor position on terminal. 483 /// only works if this is active widget. 484 /// set x or y or both to negative to hide cursor 485 void requestCursorPos(int x, int y){ 486 _requestCursorPos(x, y); 487 } 488 489 /// called to request to scrollX 490 bool requestScrollX(uint x){ 491 return _requestScrollX(x); 492 } 493 /// called to request to scrollY 494 bool requestScrollY(uint y){ 495 return _requestScrollY(y); 496 } 497 498 /// Called after UI has been run 499 bool initialize(){ 500 return false; 501 } 502 /// Called when mouse is clicked with cursor on this widget. 503 bool mouseEvent(MouseEvent mouse){ 504 return false; 505 } 506 /// Called when key is pressed and this widget is active. 507 /// 508 /// `cycle` indicates if widget cycling should happen, if this 509 /// widget has child widgets 510 bool keyboardEvent(KeyboardEvent key, bool cycle){ 511 return false; 512 } 513 /// Called when widget size is changed, 514 bool resizeEvent(){ 515 return false; 516 } 517 /// Called when the widget is rescrolled, ~but size not changed.~ 518 bool scrollEvent(){ 519 return false; 520 } 521 /// called right after this widget is activated, or de-activated 522 bool activateEvent(bool isActive){ 523 return false; 524 } 525 /// called often. 526 /// 527 /// `msecs` is the msecs since last timerEvent, not accurate 528 bool timerEvent(uint msecs){ 529 return false; 530 } 531 /// Called when this widget should re-draw itself 532 bool updateEvent(){ 533 return false; 534 } 535 public: 536 /// To request parent to trigger an update event 537 void requestUpdate(){ 538 _requestUpdate(); 539 } 540 /// To request parent to trigger a resize event 541 void requestResize(){ 542 _requestResize(); 543 } 544 /// custom initialize event 545 final @property InitFunction onInitEvent(InitFunction func){ 546 return _customInitEvent = func; 547 } 548 /// custom mouse event 549 final @property MouseEventFuction onMouseEvent(MouseEventFuction func){ 550 return _customMouseEvent = func; 551 } 552 /// custom keyboard event 553 final @property KeyboardEventFunction onKeyboardEvent( 554 KeyboardEventFunction func){ 555 return _customKeyboardEvent = func; 556 } 557 /// custom resize event 558 final @property ResizeEventFunction onResizeEvent(ResizeEventFunction func){ 559 return _customResizeEvent = func; 560 } 561 /// custom scroll event 562 final @property ScrollEventFunction onScrollEvent(ScrollEventFunction func){ 563 return _customScrollEvent = func; 564 } 565 /// custom activate event 566 final @property ActivateEventFunction onActivateEvent( 567 ActivateEventFunction func){ 568 return _customActivateEvent = func; 569 } 570 /// custom timer event 571 final @property TimerEventFunction onTimerEvent(TimerEventFunction func){ 572 return _customTimerEvent = func; 573 } 574 /// Returns: true if this widget is the current active widget 575 final @property bool isActive(){ 576 return _isActive; 577 } 578 /// Returns: true if this widget is a container that supports 579 /// scrolling 580 final @property bool isScrollableContainer(){ 581 return _isScrollableContainer; 582 } 583 /// Returns: EventMask of subscribed events 584 final @property uint eventSub(){ 585 return _eventSub; 586 } 587 /// ratio of height or width 588 final @property uint sizeRatio(){ 589 return _sizeRatio; 590 } 591 /// ditto 592 final @property uint sizeRatio(uint newRatio){ 593 _sizeRatio = newRatio; 594 _requestResize(); 595 return _sizeRatio; 596 } 597 /// if widget is visible. 598 final @property bool show(){ 599 return _show; 600 } 601 /// ditto 602 final @property bool show(bool visibility){ 603 _show = visibility; 604 _requestResize(); 605 return _show; 606 } 607 /// horizontal scroll. 608 final @property uint scrollX(){ 609 return _scrollX; 610 } 611 /// ditto 612 final @property uint scrollX(uint newVal){ 613 _requestScrollX(newVal); 614 return _scrollX; 615 } 616 /// vertical scroll. 617 final @property uint scrollY(){ 618 return _scrollY; 619 } 620 /// ditto 621 final @property uint scrollY(uint newVal){ 622 _requestScrollY(newVal); 623 return _scrollY; 624 } 625 /// width of widget 626 final @property uint width(){ 627 return _width; 628 } 629 /// ditto 630 @property uint width(uint value){ 631 _minWidth = value; 632 _maxWidth = value; 633 _requestResize(); 634 return value; 635 } 636 /// height of widget 637 final @property uint height(){ 638 return _height; 639 } 640 /// ditto 641 @property uint height(uint value){ 642 _minHeight = value; 643 _maxHeight = value; 644 _requestResize(); 645 return value; 646 } 647 /// minimum width 648 @property uint minWidth(){ 649 return _minWidth; 650 } 651 /// ditto 652 @property uint minWidth(uint value){ 653 _requestResize(); 654 return _minWidth = value; 655 } 656 /// minimum height 657 @property uint minHeight(){ 658 return _minHeight; 659 } 660 /// ditto 661 @property uint minHeight(uint value){ 662 _requestResize(); 663 return _minHeight = value; 664 } 665 /// maximum width 666 @property uint maxWidth(){ 667 return _maxWidth; 668 } 669 /// ditto 670 @property uint maxWidth(uint value){ 671 _requestResize(); 672 return _maxWidth = value; 673 } 674 /// maximum height 675 @property uint maxHeight(){ 676 return _maxHeight; 677 } 678 /// ditto 679 @property uint maxHeight(uint value){ 680 _requestResize(); 681 return _maxHeight = value; 682 } 683 } 684 685 /// Used to place widgets in an order (i.e vertical or horizontal) 686 class QLayout : QWidget{ 687 private: 688 /// widgets 689 QWidget[] _widgets; 690 /// layout type, horizontal or vertical 691 QLayout.Type _type; 692 /// index of active widget. -1 if none. 693 int _activeWidgetIndex = -1; 694 /// if it is overflowing 695 bool _isOverflowing = false; 696 /// Color to fill with in unoccupied space 697 Color _fillColor; 698 /// Color to fill with when overflowing 699 Color _overflowColor = DEFAULT_OVERFLOW_BG; 700 701 /// gets height/width of a widget using: 702 /// it's sizeRatio and min/max-height/width 703 uint _calculateWidgetSize(QWidget widget, uint ratioTotal, 704 uint totalSpace, ref bool free){ 705 immutable uint calculatedSize = 706 cast(uint)(widget.sizeRatio * totalSpace / ratioTotal); 707 if (_type == QLayout.Type.Horizontal){ 708 free = widget.minWidth == 0 && widget.maxWidth == 0; 709 return getLimitedSize(calculatedSize, widget.minWidth, widget.maxWidth); 710 } 711 free = widget.minHeight == 0 && widget.maxHeight == 0; 712 return getLimitedSize(calculatedSize, widget.minHeight, widget.maxHeight); 713 } 714 715 /// recalculates the size of every widget inside layout 716 void _recalculateWidgetsSize(){ 717 FIFOStack!QWidget widgetStack = new FIFOStack!QWidget; 718 uint totalRatio = 0; 719 uint totalSpace = _type == QLayout.Type.Horizontal ? _width : _height; 720 bool free = false; 721 foreach (widget; _widgets){ 722 if (!widget.show) 723 continue; 724 totalRatio += widget.sizeRatio; 725 widget._height = getLimitedSize(_height, widget.minHeight, 726 widget.maxHeight); 727 728 widget._width = getLimitedSize(_width, widget.minWidth, 729 widget.maxWidth); 730 widgetStack.push(widget); 731 } 732 // do widgets with size limits 733 /// totalRatio, and space used of widgets with limits 734 uint limitWRatio, limitWSize; 735 for (int i = 0; i < widgetStack.count; i ++){ 736 QWidget widget = widgetStack.pop; 737 immutable uint space = _calculateWidgetSize(widget, totalRatio, 738 totalSpace, free); 739 if (free){ 740 widgetStack.push(widget); 741 continue; 742 } 743 if (_type == QLayout.Type.Horizontal) 744 widget._width = space; 745 else 746 widget._height = space; 747 limitWRatio += widget.sizeRatio; 748 limitWSize += space; 749 } 750 totalSpace -= limitWSize; 751 totalRatio -= limitWRatio; 752 while (widgetStack.count){ 753 QWidget widget = widgetStack.pop; 754 immutable uint space = _calculateWidgetSize(widget, 755 totalRatio, totalSpace, free); 756 if (_type == QLayout.Type.Horizontal) 757 widget._width = space; 758 else 759 widget._height = space; 760 totalRatio -= widget.sizeRatio; 761 totalSpace -= space; 762 } 763 .destroy(widgetStack); 764 } 765 766 /// find the next widget to activate 767 /// Returns: index, or -1 if no one wanna be active 768 int _nextActiveWidget(){ 769 for (int i = _activeWidgetIndex + 1; i < _widgets.length; i ++){ 770 if ((_widgets[i].eventSub & EventMask.KeyboardAll) && _widgets[i].show) 771 return i; 772 } 773 return -1; 774 } 775 protected: 776 override void eventSubscribe(){ 777 _eventSub = 0; 778 foreach (widget; _widgets) 779 _eventSub |= widget.eventSub; 780 781 // if children can become active, then need activate too 782 if (_eventSub & EventMask.KeyboardAll) 783 _eventSub |= EventMask.Activate; 784 785 _eventSub |= EventMask.Scroll; 786 787 if (_parent) 788 _parent.eventSubscribe(); 789 } 790 791 /// Recalculates size and position for all visible widgets 792 override bool resizeEvent(){ 793 // resize everything 794 _recalculateWidgetsSize(); 795 // position everything, and grow if can 796 uint space, maxW, maxH; 797 foreach (widget; _widgets){ 798 if (!widget.show) 799 continue; 800 widget._posX = widget._posY = 0; 801 maxH = maxH < widget.height ? widget.height : maxH; 802 maxW = maxW < widget.width ? widget.width : maxW; 803 if (_type == Type.Horizontal){ 804 widget._posX = space; 805 space += widget.width; 806 }else{ 807 widget._posY = space; 808 space += widget.height; 809 } 810 } 811 if (maxW > width || maxH > height){ 812 // if parent is scrollable container and there no size limits 813 // then grow to required size 814 // TODO grow till maxWidth or maxHeight reached 815 if (minHeight + maxHeight + minWidth + maxWidth == 0 && _parent && 816 _parent.isScrollableContainer){ 817 _isOverflowing = false; 818 _height = maxH; 819 _width = maxW; 820 }else{ 821 _isOverflowing = true; 822 } 823 } 824 if (_isOverflowing){ 825 foreach (widget; _widgets) 826 widget._view._reset(); 827 }else{ 828 foreach (i, widget; _widgets){ 829 _view._getSlice(widget._view, widget._posX, widget._posY, 830 widget.width, widget.height); 831 widget._resizeEventCall(); 832 } 833 } 834 return true; 835 } 836 837 /// Resize event 838 override bool scrollEvent(){ 839 if (_isOverflowing){ 840 foreach (widget; _widgets) 841 widget._view._reset(); 842 return false; 843 } 844 foreach (i, widget; _widgets){ 845 _view._getSlice(widget._view, widget._posX, widget._posY, 846 widget.width, widget.height); 847 widget._scrollEventCall(); 848 } 849 return true; 850 } 851 852 /// Redirects the mouseEvent to the appropriate widget 853 override public bool mouseEvent(MouseEvent mouse){ 854 if (_isOverflowing) 855 return false; 856 int index; 857 if (_type == Type.Horizontal){ 858 foreach (i, w; _widgets){ 859 if (w.show && w._posX <= mouse.x && w._posX + w.width > mouse.x){ 860 index = cast(int)i; 861 break; 862 } 863 } 864 }else{ 865 foreach (i, w; _widgets){ 866 if (w.show && w._posY <= mouse.y && w._posY + w.height > mouse.y){ 867 index = cast(int)i; 868 break; 869 } 870 } 871 } 872 if (index > -1){ 873 if (mouse.state != MouseEvent.State.Hover && 874 index != _activeWidgetIndex && 875 _widgets[index].eventSub & EventMask.KeyboardAll){ 876 if (_activeWidgetIndex > -1) 877 _widgets[_activeWidgetIndex]._activateEventCall(false); 878 _widgets[index]._activateEventCall(true); 879 _activeWidgetIndex = index; 880 } 881 return _widgets[index]._mouseEventCall(mouse); 882 } 883 return false; 884 } 885 886 /// Redirects the keyboardEvent to appropriate widget 887 override public bool keyboardEvent(KeyboardEvent key, bool cycle){ 888 if (_isOverflowing) 889 return false; 890 if (_activeWidgetIndex > -1 && 891 _widgets[_activeWidgetIndex]._keyboardEventCall(key, cycle)) 892 return true; 893 894 if (!cycle) 895 return false; 896 immutable int next = _nextActiveWidget(); 897 898 if (_activeWidgetIndex > -1 && next != _activeWidgetIndex) 899 _widgets[_activeWidgetIndex]._activateEventCall(false); 900 901 if (next == -1) 902 return false; 903 _activeWidgetIndex = next; 904 _widgets[_activeWidgetIndex]._activateEventCall(true); 905 return true; 906 } 907 908 /// initialise 909 override bool initialize(){ 910 foreach (widget; _widgets){ 911 widget._view._reset(); 912 widget._initializeCall(); 913 } 914 return true; 915 } 916 917 /// timer 918 override bool timerEvent(uint msecs){ 919 foreach (widget; _widgets) 920 widget._timerEventCall(msecs); 921 return true; 922 } 923 924 /// activate event 925 override bool activateEvent(bool isActive){ 926 if (isActive){ 927 _activeWidgetIndex = -1; 928 _activeWidgetIndex = _nextActiveWidget(); 929 } 930 if (_activeWidgetIndex > -1) 931 _widgets[_activeWidgetIndex]._activateEventCall(isActive); 932 return true; 933 } 934 935 /// called by parent widget to update 936 override bool updateEvent(){ 937 if (_isOverflowing){ 938 foreach (y; viewportY .. viewportY + viewportHeight){ 939 moveTo(viewportX, y); 940 fillLine(' ', DEFAULT_FG, _overflowColor); 941 } 942 return false; 943 } 944 if (_type == Type.Horizontal){ 945 foreach(i, widget; _widgets){ 946 if (widget.show && widget._requestingUpdate) 947 widget._updateEventCall(); 948 foreach (y; widget.height .. viewportY + viewportHeight){ 949 moveTo(widget._posX, y); 950 fillLine(' ', DEFAULT_FG, _fillColor, 951 widget.width); 952 } 953 } 954 }else{ 955 foreach(i, widget; _widgets){ 956 if (widget.show && widget._requestingUpdate) 957 widget._updateEventCall(); 958 if (widget.width == _width) 959 continue; 960 foreach (y; widget._posY .. widget._posY + widget.height){ 961 moveTo(widget._posX + widget.width, y); 962 fillLine(' ', DEFAULT_FG, _fillColor); 963 } 964 } 965 } 966 return true; 967 } 968 969 /// activate the passed widget if it's in the current layout 970 /// Returns: true if it was activated or not 971 override bool searchAndActivateWidget(QWidget target){ 972 immutable int lastActiveWidgetIndex = _activeWidgetIndex; 973 974 // search and activate recursively 975 _activeWidgetIndex = -1; 976 foreach (index, widget; _widgets) { 977 if ((widget.eventSub & EventMask.KeyboardAll) && 978 widget.show && 979 widget.searchAndActivateWidget(target)){ 980 _activeWidgetIndex = cast(int)index; 981 break; 982 } 983 } 984 985 // and then manipulate the current layout 986 if (lastActiveWidgetIndex != _activeWidgetIndex && 987 lastActiveWidgetIndex > -1) 988 _widgets[lastActiveWidgetIndex]._activateEventCall(false); 989 return _activeWidgetIndex != -1; 990 } 991 992 public: 993 /// Layout type 994 enum Type{ 995 Vertical, 996 Horizontal, 997 } 998 /// constructor 999 this(QLayout.Type type){ 1000 _type = type; 1001 this._fillColor = DEFAULT_BG; 1002 } 1003 /// destructor, kills children 1004 ~this(){ 1005 foreach (child; _widgets) 1006 .destroy(child); 1007 } 1008 /// Color for unoccupied space 1009 @property Color fillColor(){ 1010 return _fillColor; 1011 } 1012 /// ditto 1013 @property Color fillColor(Color newColor){ 1014 return _fillColor = newColor; 1015 } 1016 /// Color to fill with when out of space 1017 @property Color overflowColor(){ 1018 return _overflowColor; 1019 } 1020 /// ditto 1021 @property Color overflowColor(Color newColor){ 1022 return _overflowColor = newColor; 1023 } 1024 /// adds a widget 1025 /// 1026 /// `allowScrollControl` specifies if the widget will be able 1027 /// to make scrolling requests 1028 void addWidget(QWidget widget, bool allowScrollControl = false){ 1029 widget._parent = this; 1030 widget._canReqScroll = allowScrollControl; 1031 _widgets ~= widget; 1032 eventSubscribe(); 1033 } 1034 /// ditto 1035 void addWidget(QWidget[] widgets){ 1036 foreach (i, widget; widgets){ 1037 widget._parent = this; 1038 } 1039 _widgets ~= widgets; 1040 eventSubscribe(); 1041 } 1042 } 1043 1044 /// A Scrollable Container 1045 class ScrollContainer : QWidget{ 1046 private: 1047 /// offset in _widget before adding scrolling to it 1048 uint _offX, _offY; 1049 protected: 1050 /// widget 1051 QWidget _widget; 1052 /// if scrollbar is to be shown 1053 bool _scrollbarV, _scrollbarH; 1054 /// if page down/up button should scroll 1055 bool _pgDnUp = false; 1056 /// if mouse wheel should scroll 1057 bool _mouseWheel = false; 1058 /// height and width available to widget 1059 uint _drawAreaHeight, _drawAreaWidth; 1060 /// Scrollbar colors 1061 Color _sbarBg = DEFAULT_BG, _sbarFg = DEFAULT_FG; 1062 1063 override void eventSubscribe(){ 1064 if (_widget) 1065 _eventSub |= _widget.eventSub; 1066 _eventSub |= EventMask.Resize | EventMask.Scroll | 1067 (EventMask.KeyboardPress * _pgDnUp) | 1068 (EventMask.MouseAll * _mouseWheel) | 1069 (EventMask.Update * (_scrollbarH || _scrollbarV)); 1070 if (_parent) 1071 _parent.eventSubscribe(); 1072 } 1073 1074 /// re-assings display buffer based on _subScrollX/Y, 1075 /// and calls scrollEvent on child if `callScrollEvents` 1076 final void rescroll(bool callScrollEvent = true){ 1077 if (!_widget) 1078 return; 1079 _widget._view._reset(); 1080 if (_width == 0 || _height == 0) 1081 return; 1082 uint w = _drawAreaWidth, h = _drawAreaHeight; 1083 if (w > _widget.width) 1084 w = _widget.width; 1085 if (h > _widget.height) 1086 h = _widget.height; 1087 _view._getSlice(_widget._view, 0, 0, w, h); 1088 _widget._view._offsetX = _offX + _widget.scrollX; 1089 _widget._view._offsetY = _offY + _widget.scrollY; 1090 if (callScrollEvent) 1091 _widget._scrollEventCall(); 1092 } 1093 1094 override bool requestScrollX(uint x){ 1095 if (!_widget) 1096 return false; 1097 if (_widget.width <= _drawAreaWidth) 1098 x = 0; 1099 else if (x > _widget.width - _drawAreaWidth) 1100 x = _widget.width - _drawAreaWidth; 1101 1102 if (_widget.scrollX == x) 1103 return false; 1104 _widget._scrollX = x; 1105 rescroll(); 1106 return true; 1107 } 1108 override bool requestScrollY(uint y){ 1109 if (!_widget) 1110 return false; 1111 if (_widget.height <= _drawAreaHeight) 1112 y = 0; 1113 else if (y > _widget.height - _drawAreaHeight) 1114 y = _widget.height - _drawAreaHeight; 1115 1116 if (_widget.scrollY == y) 1117 return false; 1118 _widget._scrollY = y; 1119 rescroll(); 1120 return true; 1121 } 1122 1123 override bool resizeEvent(){ 1124 _offX = _view._offsetX; 1125 _offY = _view._offsetY; 1126 _drawAreaHeight = _height - (1 * (_height > 0 && _scrollbarV)); 1127 _drawAreaWidth = _width - (1 * (_width > 0 && _scrollbarV)); 1128 if (!_widget) 1129 return false; 1130 if (_scrollbarH || _scrollbarV) 1131 requestUpdate(); 1132 // try to size widget to fit 1133 if (_height > 0 && _width > 0){ 1134 _widget._width = getLimitedSize(_drawAreaWidth, 1135 _widget.minWidth, _widget.maxWidth); 1136 _widget._height = getLimitedSize(_drawAreaHeight, 1137 _widget.minHeight, _widget.maxHeight); 1138 } 1139 rescroll(false); 1140 _widget._resizeEventCall(); 1141 1142 return true; 1143 } 1144 1145 override bool scrollEvent(){ 1146 _offX = _view._offsetX; 1147 _offY = _view._offsetY; 1148 if (_scrollbarH || _scrollbarV) 1149 requestUpdate(); 1150 rescroll(); 1151 1152 return true; 1153 } 1154 1155 override bool keyboardEvent(KeyboardEvent key, bool cycle){ 1156 if (!_widget) 1157 return false; 1158 if (_widget.isActive && _widget._keyboardEventCall(key, cycle)) 1159 return true; 1160 1161 if (!cycle && _pgDnUp && key.state == KeyboardEvent.State.Pressed){ 1162 if (key.key == Key.PageUp){ 1163 return requestScrollY( 1164 _drawAreaHeight > _widget.scrollY ? 0 : 1165 _widget.scrollY - _drawAreaHeight); 1166 } 1167 if (key.key == Key.PageDown){ 1168 return requestScrollY( 1169 _drawAreaHeight + _widget.scrollY > _widget.height ? 1170 _widget.height - _drawAreaHeight : 1171 _widget.scrollY + _drawAreaHeight); 1172 } 1173 } 1174 _widget._activateEventCall(false); 1175 return false; 1176 } 1177 1178 override bool mouseEvent(MouseEvent mouse){ 1179 if (!_widget) 1180 return false; 1181 if (_widget._mouseEventCall(mouse)){ 1182 _widget._activateEventCall(true); 1183 return true; 1184 } 1185 _widget._activateEventCall(false); 1186 if (_mouseWheel && _drawAreaHeight < _widget.height){ 1187 if (mouse.button == mouse.Button.ScrollUp){ 1188 if (_widget.scrollY) 1189 return requestScrollY(_widget.scrollY - 1); 1190 } 1191 if (mouse.button == mouse.Button.ScrollDown){ 1192 return requestScrollY(_widget.scrollY + 1); 1193 } 1194 } 1195 return false; 1196 } 1197 1198 override bool updateEvent(){ 1199 if (!_widget) 1200 return false; 1201 if (_widget.show) 1202 _widget._updateEventCall(); 1203 // TODO: fill unoccupied space, if any 1204 drawScrollbars(); 1205 1206 return true; 1207 } 1208 1209 /// draws scrollbars, very basic stuff 1210 void drawScrollbars(){ 1211 if (!_widget || _width == 0 || _height == 0) 1212 return; 1213 static const dchar verticalLine = '│', horizontalLine = '─'; 1214 static const dchar block = '█'; 1215 if (_scrollbarH && _scrollbarV){ 1216 moveTo(_width - 1, _height - 1); 1217 write('┘', _sbarFg, _sbarBg); 1218 } 1219 if (_scrollbarH){ 1220 moveTo(0, _drawAreaHeight); 1221 fillLine(horizontalLine, _sbarFg, _sbarBg, 1222 _drawAreaWidth); 1223 const int maxScroll = _widget.width - _drawAreaWidth; 1224 if (maxScroll > 0){ 1225 const uint barPos = (_widget.scrollX * _drawAreaWidth) / maxScroll; 1226 moveTo(barPos, _drawAreaHeight); 1227 write(block, _sbarFg, _sbarBg); 1228 } 1229 } 1230 if (_scrollbarV){ 1231 foreach (y; 0 .. _drawAreaHeight){ 1232 moveTo(_drawAreaWidth, y); 1233 write(verticalLine, _sbarFg, _sbarBg); 1234 } 1235 const int maxScroll = _widget.height - _drawAreaHeight; 1236 if (maxScroll > 0){ 1237 const uint barPos = (_widget.scrollY * _drawAreaHeight) / maxScroll; 1238 moveTo(_drawAreaWidth, barPos); 1239 write(block, _sbarFg, _sbarBg); 1240 } 1241 } 1242 } 1243 1244 public: 1245 /// constructor 1246 this(){ 1247 this._isScrollableContainer = true; 1248 this._scrollbarV = true; 1249 this._scrollbarH = true; 1250 } 1251 ~this(){ 1252 if (_widget) 1253 .destroy(_widget); 1254 } 1255 1256 /// Scrollbar foreground color 1257 @property Color scrollbarForeground(){ 1258 return _sbarFg; 1259 } 1260 /// ditto 1261 @property Color scrollbarForeground(Color newVal){ 1262 _requestUpdate(); 1263 return _sbarFg = newVal; 1264 } 1265 /// Scrollbar background color 1266 @property Color scrollbarBackground(){ 1267 return _sbarBg; 1268 } 1269 /// ditto 1270 @property Color scrollbarBackground(Color newVal){ 1271 _requestUpdate(); 1272 return _sbarBg = newVal; 1273 } 1274 1275 /// Sets the child widget. 1276 /// 1277 /// Returns: false if already has a child 1278 bool setWidget(QWidget child){ 1279 if (_widget) 1280 return false; 1281 _widget = child; 1282 _widget._parent = this; 1283 _widget._canReqScroll = true; 1284 _widget._posX = 0; 1285 _widget._posY = 0; 1286 eventSubscribe(); 1287 return true; 1288 } 1289 1290 override void requestResize(){ 1291 // just do a the resize within itself 1292 _resizeEventCall(); 1293 } 1294 1295 /// Whether to scroll on page up/down keys 1296 @property bool scrollOnPageUpDown(){ 1297 return _pgDnUp; 1298 } 1299 /// ditto 1300 @property bool scrollOnPageUpDown(bool newVal){ 1301 _pgDnUp = newVal; 1302 eventSubscribe(); 1303 return _pgDnUp; 1304 } 1305 1306 /// Whether to scroll on mouse scroll wheel 1307 @property bool scrollOnMouseWheel(){ 1308 return _mouseWheel; 1309 } 1310 /// ditto 1311 @property bool scrollOnMouseWheel(bool newVal){ 1312 _mouseWheel = newVal; 1313 eventSubscribe(); 1314 return _mouseWheel; 1315 } 1316 1317 /// Whether to show vertical scrollbar. 1318 /// 1319 /// Modifying this will request update 1320 @property bool scrollbarV(){ 1321 return _scrollbarV; 1322 } 1323 /// ditto 1324 @property bool scrollbarV(bool newVal){ 1325 if (newVal != _scrollbarV){ 1326 _scrollbarV = newVal; 1327 rescroll(); 1328 } 1329 return _scrollbarV; 1330 } 1331 /// Whether to show horizontal scrollbar. 1332 /// 1333 /// Modifying this will request update 1334 @property bool scrollbarH(){ 1335 return _scrollbarH; 1336 } 1337 /// ditto 1338 @property bool scrollbarH(bool newVal){ 1339 if (newVal != _scrollbarH){ 1340 _scrollbarH = newVal; 1341 rescroll(); 1342 } 1343 return _scrollbarH; 1344 } 1345 } 1346 1347 /// Terminal 1348 class QTerminal : QLayout{ 1349 private: 1350 /// To actually access the terminal 1351 TermWrapper _termWrap; 1352 /// set to false to stop UI loop in run() 1353 bool _isRunning; 1354 /// the key used for cycling active widget 1355 dchar _activeWidgetCycleKey = WIDGET_CYCLE_KEY; 1356 /// whether to stop UI loop on Interrupt 1357 bool _stopOnInterrupt = true; 1358 /// cursor position 1359 int _cursorX = -1, _cursorY = -1; 1360 1361 /// Reads InputEvent and calls appropriate functions 1362 void _readEvent(Event event){ 1363 if (event.type == Event.Type.HangupInterrupt){ 1364 if (_stopOnInterrupt) 1365 _isRunning = false; 1366 else{ // otherwise read it as a Ctrl+C 1367 KeyboardEvent keyEvent; 1368 keyEvent.key = KeyboardEvent.CtrlKeys.CtrlC; 1369 this._keyboardEventCall(keyEvent, false); 1370 } 1371 }else if (event.type == Event.Type.Keyboard){ 1372 KeyboardEvent kPress = event.keyboard; 1373 this._keyboardEventCall(kPress, false); 1374 }else if (event.type == Event.Type.Mouse){ 1375 this._mouseEventCall(event.mouse); 1376 }else if (event.type == Event.Type.Resize){ 1377 this._resizeEventCall(); 1378 } 1379 } 1380 1381 /// writes _view to _termWrap 1382 void _flushBuffer(){ 1383 if (_view._buffer.length == 0) 1384 return; 1385 Cell prev = _view._buffer[0]; 1386 _termWrap.color(prev.fg, prev.bg); 1387 uint x, y; 1388 foreach (cell; _view._buffer){ 1389 if (!prev.colorsSame(cell)) 1390 _termWrap.color(cell.fg, cell.bg); 1391 prev = cell; 1392 _termWrap.put(x, y, cell.c); 1393 x ++; 1394 if (x == _width){ 1395 x = 0; 1396 y ++; 1397 } 1398 } 1399 } 1400 1401 protected: 1402 1403 override void eventSubscribe(){ 1404 // ignore what children want, this needs all events 1405 // so custom event handlers can be set up 1406 _eventSub = uint.max; 1407 } 1408 1409 override void requestCursorPos(int x, int y){ 1410 _cursorX = x; 1411 _cursorY = y; 1412 } 1413 1414 override bool resizeEvent(){ 1415 _height = _termWrap.height; 1416 _width = _termWrap.width; 1417 _view._buffer.length = _width * _height; 1418 _view._width = _width; 1419 _view._actualWidth = _width; 1420 _view._height = _height; 1421 super.resizeEvent(); 1422 return true; 1423 } 1424 1425 override bool keyboardEvent(KeyboardEvent key, bool cycle){ 1426 cycle = key.state == KeyboardEvent.State.Pressed && 1427 key.key == _activeWidgetCycleKey; 1428 return super.keyboardEvent(key, cycle); 1429 } 1430 1431 override bool updateEvent(){ 1432 // resize if needed 1433 if (_requestingResize) 1434 this._resizeEventCall(); 1435 _cursorX = -1; 1436 _cursorY = -1; 1437 // no, this is not a mistake, dont change this to updateEventCall again 1438 super.updateEvent(); 1439 // flush _view._buffer to _termWrap 1440 _flushBuffer(); 1441 // check if need to show/hide cursor 1442 if (_cursorX < 0 || _cursorY < 0) 1443 _termWrap.cursorVisible = false; 1444 else{ 1445 _termWrap.moveCursor(_cursorX, _cursorY); 1446 _termWrap.cursorVisible = true; 1447 } 1448 _termWrap.flush(); 1449 return true; 1450 } 1451 1452 public: 1453 /// time to wait between timer events (milliseconds) 1454 ushort timerMsecs; 1455 /// constructor 1456 this(QLayout.Type displayType = QLayout.Type.Vertical, 1457 ushort timerDuration = 500){ 1458 // HACK: fix for issue #18 (resizing on alacritty borked) 1459 if (environment["TERM"] == "alacritty") 1460 environment["TERM"] = "xterm"; 1461 super(displayType); 1462 timerMsecs = timerDuration; 1463 1464 _termWrap = new TermWrapper(); 1465 // so it can make other widgets active on mouse events 1466 this._isActive = true; 1467 } 1468 ~this(){ 1469 .destroy(_termWrap); 1470 } 1471 1472 /// stops UI loop. **not instant** 1473 /// if it is in-between event functions, it will complete 1474 /// those first 1475 void terminate(){ 1476 _isRunning = false; 1477 } 1478 1479 /// whether to stop UI loop on HangupInterrupt (Ctrl+C) 1480 @property bool terminateOnHangup(){ 1481 return _stopOnInterrupt; 1482 } 1483 /// ditto 1484 @property bool terminateOnHangup(bool newVal){ 1485 return _stopOnInterrupt = newVal; 1486 } 1487 1488 /// starts the UI loop 1489 void run(){ 1490 _initializeCall(); 1491 _resizeEventCall(); 1492 _updateEventCall(); 1493 _isRunning = true; 1494 StopWatch sw = StopWatch(AutoStart.yes); 1495 while (_isRunning){ 1496 int timeout = cast(int) 1497 (timerMsecs - sw.peek.total!"msecs"); 1498 Event event; 1499 while (_termWrap.getEvent(timeout, event) > 0){ 1500 _readEvent(event); 1501 timeout = cast(int) 1502 (timerMsecs - sw.peek.total!"msecs"); 1503 _updateEventCall(); 1504 } 1505 if (sw.peek.total!"msecs" >= timerMsecs){ 1506 _timerEventCall(cast(uint)sw.peek.total!"msecs"); 1507 sw.reset; 1508 sw.start; 1509 _updateEventCall(); 1510 } 1511 } 1512 } 1513 1514 /// search the passed widget recursively and activate it 1515 /// 1516 /// Returns: true if the widget was made active, false if not 1517 bool activateWidget(QWidget target) { 1518 return this.searchAndActivateWidget(target); 1519 } 1520 1521 /// Changes the key used to cycle between active widgets. 1522 void setActiveWidgetCycleKey(dchar key){ 1523 _activeWidgetCycleKey = key; 1524 } 1525 }