1 /++ 2 This module contains most of the functions you'll need. 3 All the 'base' classes, like QWidget are defined in this. 4 +/ 5 module qui.qui; 6 7 import std.datetime.stopwatch; 8 import utils.misc; 9 import utils.lists; 10 import std.conv : to; 11 import qui.termwrap; 12 13 import qui.utils; 14 15 /// the default foreground color 16 const Color DEFAULT_FG = Color.white; 17 /// the default background color 18 const Color DEFAULT_BG = Color.black; 19 20 /// Available colors are in this enum 21 public alias Color = qui.termwrap.Color; 22 /// Availabe Keys (keyboard) for input 23 public alias Key = qui.termwrap.Event.Keyboard.Key; 24 /// Mouse Event 25 public alias MouseEvent = Event.Mouse; 26 /// Keyboard Event 27 public alias KeyboardEvent = Event.Keyboard; 28 29 /// Used to store position for widgets 30 struct Position{ 31 /// x and y position 32 integer x = 0, y = 0; 33 /// Returns: a string representation of Position 34 string tostring(){ 35 return "{x:"~to!string(x)~",y:"~to!string(y)~"}"; 36 } 37 } 38 39 /// To store size for widgets 40 /// 41 /// zero in min/max means no limit 42 struct Size{ 43 private{ 44 uinteger _w = 0, _h = 0; 45 /// width 46 @property uinteger width(uinteger newWidth){ 47 if (newWidth == 0) 48 return _w = 0; 49 if (minWidth > 0 && newWidth < minWidth){ 50 return _w = minWidth; 51 }else if (maxWidth > 0 && newWidth > maxWidth){ 52 return _w = maxWidth; 53 } 54 return _w = newWidth; 55 } 56 /// height 57 @property uinteger height(uinteger newHeight){ 58 if (newHeight == 0) 59 return _h = 0; 60 if (minHeight > 0 && newHeight < minHeight){ 61 return _h = minHeight; 62 }else if (maxHeight > 0 && newHeight > maxHeight){ 63 return _h = maxHeight; 64 } 65 return _h = newHeight; 66 } 67 } 68 /// width 69 @property uinteger width(){ 70 return _w; 71 } 72 /// height 73 @property uinteger height(){ 74 return _h; 75 } 76 77 /// minimun width & height. These are "applied" automatically when setting value using `width` or `height` 78 uinteger minWidth = 0, minHeight = 0; 79 /// maximum width & height. These are "applied" automatically when setting value using `width` or `height` 80 uinteger maxWidth = 0, maxHeight = 0; 81 /// Returns: a string representation of KeyPress, in JSON 82 string tostring(){ 83 return "{width:"~to!string(_w)~",height:"~to!string(_h)~ 84 ",minWidth:"~to!string(minWidth)~",maxWidth:"~to!string(maxWidth)~ 85 ",minHeight:"~to!string(minHeight)~",maxHeight:"~to!string(maxHeight)~"}"; 86 } 87 } 88 89 /// mouseEvent function. Return true if the event should be dropped 90 alias MouseEventFuction = bool delegate(QWidget, MouseEvent); 91 ///keyboardEvent function. Return true if the event should be dropped 92 alias KeyboardEventFunction = bool delegate(QWidget, KeyboardEvent); 93 /// resizeEvent function. Return true if the event should be dropped 94 alias ResizeEventFunction = bool delegate(QWidget); 95 /// activateEvent function. Return true if the event should be dropped 96 alias ActivateEventFunction = bool delegate(QWidget, bool); 97 /// TimerEvent function. Return true if the event should be dropped 98 alias TimerEventFunction = bool delegate(QWidget, uinteger); 99 /// Init function. Return true if the event should be dropped 100 alias InitFunction = bool delegate(QWidget); 101 102 103 /// Base class for all widgets, including layouts and QTerminal 104 /// 105 /// Use this as parent-class for new widgets 106 abstract class QWidget{ 107 private: 108 /// stores the position of this widget, relative to it's parent widget's position 109 Position _position; 110 /// stores if this widget is the active widget 111 bool _isActive = false; 112 /// stores what child widgets want updates 113 bool[] _requestingUpdate; 114 /// what key handlers are registered for what keys (key/index) 115 QWidget[dchar] _keyHandlers; 116 /// the parent widget 117 QWidget _parent = null; 118 /// the index it is stored at in _parent. -1 if no parent asigned yet 119 integer _indexInParent = -1; 120 /// the key used for cycling active widget, defaults to tab 121 dchar _activeWidgetCycleKey = '\t'; 122 /// called by owner for initialize event 123 void initializeCall(){ 124 if (!_customInitEvent || !_customInitEvent(this)) 125 this.initialize(); 126 } 127 /// called by owner for mouseEvent 128 void mouseEventCall(MouseEvent mouse){ 129 mouse.x = mouse.x - cast(int)this._position.x; 130 mouse.y = mouse.y - cast(int)this._position.y; 131 if (!_customMouseEvent || !_customMouseEvent(this, mouse)) 132 this.mouseEvent(mouse); 133 } 134 /// called by owner for keyboardEvent 135 void keyboardEventCall(KeyboardEvent key){ 136 if (!_customKeyboardEvent || !_customKeyboardEvent(this, key)) 137 this.keyboardEvent(key); 138 } 139 /// called by owner for resizeEvent 140 void resizeEventCall(){ 141 if (!_customResizeEvent || !_customResizeEvent(this)) 142 this.resizeEvent(); 143 } 144 /// called by owner for activateEvent 145 void activateEventCall(bool isActive){ 146 _isActive = isActive; 147 if (!_customActivateEvent || !_customActivateEvent(this, isActive)) 148 this.activateEvent(isActive); 149 } 150 /// called by owner for mouseEvent 151 void timerEventCall(uinteger msecs){ 152 if (!_customTimerEvent || !_customTimerEvent(this, msecs)) 153 this.timerEvent(msecs); 154 } 155 /// Called by children of this widget to request updates 156 void requestUpdate(uinteger index){ 157 if (index < _requestingUpdate.length){ 158 _requestingUpdate[index] = true; 159 requestUpdate(); 160 } 161 } 162 /// Called by children of this widget to register key handlers 163 bool registerKeyHandler(QWidget widget, dchar key){ 164 if (key in _keyHandlers || _parent is null) 165 return false; 166 _keyHandlers[key] = widget; 167 this.registerKeyHandler(key); 168 return true; 169 } 170 protected: 171 ///size of this widget 172 Size _size; 173 ///whether this widget should be drawn or not 174 bool _show = true; 175 /// specifies that how much height (in horizontal layout) or width (in vertical) is given to this widget. 176 /// The ratio of all widgets is added up and height/width for each widget is then calculated using this 177 uinteger _sizeRatio = 1; 178 /// specifies whether this widget should receive the Tab key press, default is false, and should only be changed to true 179 /// if only required, for example, in text editors 180 bool _wantsTab = false; 181 /// whether the widget wants input 182 bool _wantsInput = false; 183 /// used to write to terminal 184 Display _display = null; 185 186 /// For cycling between widgets. Returns false, always. 187 bool cycleActiveWidget(){ 188 return false; 189 } 190 /// activate the passed widget if this is actually the correct widget, return if it was activated or not 191 bool searchAndActivateWidget(QWidget target) { 192 if (this == target) { 193 this.activateEventCall(true); 194 return true; 195 } 196 return false; 197 } 198 199 /// changes the key used for cycling active widgets 200 void setActiveWidgetCycleKey(dchar newKey){ 201 this._activeWidgetCycleKey = newKey; 202 } 203 204 /// custom onInit event, if not null, it should be called before doing anything else in init(); 205 InitFunction _customInitEvent; 206 /// custom mouse event, if not null, it should be called before doing anything else in mouseEvent. 207 MouseEventFuction _customMouseEvent; 208 /// custom keyboard event, if not null, it should be called before doing anything else in keyboardEvent. 209 KeyboardEventFunction _customKeyboardEvent; 210 /// custom resize event, if not null, it should be called before doing anything else in the resizeEvent 211 ResizeEventFunction _customResizeEvent; 212 /// custom onActivate event, if not null, it should be called before doing anything else in activateEvent 213 ActivateEventFunction _customActivateEvent; 214 /// custom onTimer event, if not null, it should be called before doing anything else in timerEvent 215 TimerEventFunction _customTimerEvent; 216 217 /// Called by parent to update this widget 218 void update(){} 219 220 /// Called after `_display` has been set and this widget is ready to be used 221 void initialize(){} 222 /// Called when mouse is clicked with cursor on this widget. 223 void mouseEvent(MouseEvent mouse){} 224 /// Called when key is pressed and this widget is active. 225 void keyboardEvent(KeyboardEvent key){} 226 /// Called when widget size is changed, or widget should recalculate it's child widgets' sizes; 227 void resizeEvent(){} 228 /// called right after this widget is activated, or de-activated, i.e: is made _activeWidget, or un-made _activeWidget 229 void activateEvent(bool isActive){} 230 /// called often. `msecs` is the msecs since last timerEvent, not accurate 231 void timerEvent(uinteger msecs){} 232 public: 233 /// Called by itself when it needs to request an update 234 void requestUpdate(){ 235 if (_parent && _indexInParent > -1 && _indexInParent < _parent._requestingUpdate.length && 236 _parent._requestingUpdate[_indexInParent] == false) 237 _parent.requestUpdate(_indexInParent); 238 } 239 /// Called to request this widget to resize at next update 240 void requestResize(){ 241 if (_parent) 242 _parent.requestResize(); 243 } 244 /// Called by itself (not necessarily) to register itself as a key handler 245 bool registerKeyHandler(dchar key){ 246 return _parent && _indexInParent > -1 && _parent.registerKeyHandler(this, key); 247 } 248 /// use to change the custom initialize event 249 @property InitFunction onInitEvent(InitFunction func){ 250 return _customInitEvent = func; 251 } 252 /// use to change the custom mouse event 253 @property MouseEventFuction onMouseEvent(MouseEventFuction func){ 254 return _customMouseEvent = func; 255 } 256 /// use to change the custom keyboard event 257 @property KeyboardEventFunction onKeyboardEvent(KeyboardEventFunction func){ 258 return _customKeyboardEvent = func; 259 } 260 /// use to change the custom resize event 261 @property ResizeEventFunction onResizeEvent(ResizeEventFunction func){ 262 return _customResizeEvent = func; 263 } 264 /// use to change the custom activate event 265 @property ActivateEventFunction onActivateEvent(ActivateEventFunction func){ 266 return _customActivateEvent = func; 267 } 268 /// use to change the custom timer event 269 @property TimerEventFunction onTimerEvent(TimerEventFunction func){ 270 return _customTimerEvent = func; 271 } 272 /// Returns: this widget's parent 273 @property QWidget parent(){ 274 return _parent; 275 } 276 /// Returns: true if this widget is the current active widget 277 @property bool isActive(){ 278 return _isActive; 279 } 280 /// Returns: whether the widget is receiving the Tab key press or not 281 @property bool wantsTab(){ 282 return _wantsTab; 283 } 284 /// ditto 285 @property bool wantsTab(bool newStatus){ 286 return _wantsTab = newStatus; 287 } 288 /// Returns: true if the widget wants input 289 @property bool wantsInput(){ 290 return _wantsInput; 291 } 292 /// Returns: position of cursor to be shown on terminal. (-1,-1) if dont show 293 @property Position cursorPosition(){ 294 return Position(-1,-1); 295 } 296 /// size of width (height/width, depending of Layout.Type it is in) of this widget, in ratio to other widgets in that layout 297 @property uinteger sizeRatio(){ 298 return _sizeRatio; 299 } 300 /// ditto 301 @property uinteger sizeRatio(uinteger newRatio){ 302 requestResize; 303 return _sizeRatio = newRatio; 304 } 305 /// visibility of the widget. getter 306 @property bool show(){ 307 return _show; 308 } 309 /// visibility of the widget. setter 310 @property bool show(bool visibility){ 311 requestResize; 312 return _show = visibility; 313 } 314 /// size of the widget. getter 315 /// **NOTE: call this.requestResize() if you change the size!** 316 @property ref Size size(){ 317 return _size; 318 } 319 /// size of the widget. setter 320 @property ref Size size(Size newSize){ 321 requestResize; 322 return _size = newSize; 323 } 324 } 325 326 ///Used to place widgets in an order (i.e vertical or horizontal) 327 class QLayout : QWidget{ 328 private: 329 /// array of all the widgets that have been added to this layout 330 QWidget[] _widgets; 331 /// stores the layout type, horizontal or vertical 332 QLayout.Type _type; 333 /// stores index of active widget. -1 if none. This is useful only for Layouts. For widgets, this stays 0 334 integer _activeWidgetIndex = -1; 335 /// Color to fill with in unoccupied space 336 Color _fillColor; 337 338 /// gets height/width of a widget using it's sizeRatio and min/max-height/width 339 static uinteger calculateWidgetSize(QLayout.Type type)(QWidget widget, uinteger ratioTotal, uinteger totalSpace, 340 ref bool free){ 341 Size wSize = widget.size; 342 immutable calculatedSize = cast(uinteger)((widget.sizeRatio*totalSpace)/ratioTotal); 343 static if (type == QLayout.Type.Horizontal){ 344 wSize.width = calculatedSize; 345 free = wSize.minWidth == 0 && wSize.maxWidth == 0; 346 return wSize.width; 347 }else{ // this else just exists to shut up compiler about "statement not reachable" 348 wSize.height = calculatedSize; 349 free = wSize.minHeight == 0 && wSize.maxHeight == 0; 350 return wSize.height; 351 } 352 } 353 354 /// recalculates the size of every widget inside layout 355 void recalculateWidgetsSize(QLayout.Type T)(){ 356 static if (T != QLayout.Type.Horizontal && T != QLayout.Type.Vertical) 357 assert(false); 358 FIFOStack!QWidget widgetStack = new FIFOStack!QWidget; 359 uinteger totalRatio = 0; 360 uinteger totalSpace = T == QLayout.Type.Horizontal ? _size.width : _size.height; 361 foreach (widget; _widgets){ 362 if (!widget.show) 363 continue; 364 totalRatio += widget.sizeRatio; 365 widget._size.height = _size.height; 366 widget._size.width = _size.width; 367 widgetStack.push(widget); 368 } 369 // do widgets with size limits 370 uinteger limitWRatio, limitWSize; /// totalRatio, and space used of widgets with limits 371 for (integer i = 0; i < widgetStack.count; i ++){ 372 QWidget widget = widgetStack.pop; 373 bool free; 374 immutable uinteger space = calculateWidgetSize!(T)(widget, totalRatio, totalSpace, free); 375 if (free){ 376 widgetStack.push(widget); 377 continue; 378 } 379 static if (T == QLayout.Type.Horizontal) 380 widget._size.width = space; 381 else 382 widget._size.height = space; 383 limitWRatio += widget.sizeRatio; 384 limitWSize += space; 385 } 386 totalSpace -= limitWSize; 387 totalRatio -= limitWRatio; 388 while (widgetStack.count){ 389 QWidget widget = widgetStack.pop; 390 bool free; 391 immutable uinteger space = calculateWidgetSize!(T)(widget, totalRatio, totalSpace, free); 392 static if (T == QLayout.Type.Horizontal) 393 widget._size.width = space; 394 else 395 widget._size.height = space; 396 totalRatio -= widget.sizeRatio; 397 totalSpace -= space; 398 } 399 .destroy(widgetStack); 400 } 401 /// calculates and assigns widgets positions based on their sizes 402 void recalculateWidgetsPosition(QLayout.Type T)(){ 403 static if (T != QLayout.Type.Horizontal && T != QLayout.Type.Vertical) 404 assert(false); 405 uinteger previousSpace = 0; 406 foreach(widget; _widgets){ 407 if (widget.show){ 408 static if (T == QLayout.Type.Horizontal){ 409 widget._position.y = 0; 410 widget._position.x = previousSpace; 411 previousSpace += widget.size.width; 412 }else{ 413 widget._position.x = 0; 414 widget._position.y = previousSpace; 415 previousSpace += widget.size.height; 416 } 417 } 418 } 419 } 420 protected: 421 /// Recalculates size and position for all visible widgets 422 /// If a widget is too large to fit in, it's visibility is marked false 423 override void resizeEvent(){ 424 uinteger ratioTotal; 425 foreach(w; _widgets){ 426 if (w.show){ 427 ratioTotal += w._sizeRatio; 428 } 429 } 430 if (_type == QLayout.Type.Horizontal){ 431 recalculateWidgetsSize!(QLayout.Type.Horizontal); 432 recalculateWidgetsPosition!(QLayout.Type.Horizontal); 433 }else{ 434 recalculateWidgetsSize!(QLayout.Type.Vertical); 435 recalculateWidgetsPosition!(QLayout.Type.Vertical); 436 } 437 foreach (i, widget; _widgets){ 438 this._display.getSlice(widget._display, widget.size.width, widget.size.height,widget._position.x,widget._position.y); 439 widget.resizeEventCall(); 440 } 441 } 442 443 /// Redirects the mouseEvent to the appropriate widget 444 override public void mouseEvent(MouseEvent mouse) { 445 /// first check if it's already inside active widget, might not have to search through each widget 446 QWidget activeWidget = null; 447 if (_activeWidgetIndex > -1) 448 activeWidget = _widgets[_activeWidgetIndex]; 449 if (activeWidget && mouse.x >= activeWidget._position.x && mouse.x < activeWidget._position.x +activeWidget.size.width 450 && mouse.y >= activeWidget._position.y && mouse.y < activeWidget._position.y + activeWidget.size.height){ 451 activeWidget.mouseEventCall(mouse); 452 }else{ 453 foreach (i, widget; _widgets){ 454 if (widget.show && widget.wantsInput && 455 mouse.x >= widget._position.x && mouse.x < widget._position.x + widget.size.width && 456 mouse.y >= widget._position.y && mouse.y < widget._position.y + widget.size.height){ 457 // make it active only if this layout is itself active 458 if (this.isActive){ 459 if (activeWidget) 460 activeWidget.activateEventCall(false); 461 widget.activateEventCall(true); 462 _activeWidgetIndex = i; 463 } 464 widget.mouseEventCall(mouse); 465 break; 466 } 467 } 468 } 469 } 470 471 /// Redirects the keyboardEvent to appropriate widget 472 override public void keyboardEvent(KeyboardEvent key){ 473 // check if key handler registered 474 if (key.key in _keyHandlers) 475 _keyHandlers[key.key].keyboardEventCall(key); 476 // check if need to cycle 477 if (key.key == _activeWidgetCycleKey || key.key == KeyboardEvent.Key.Escape){ 478 if (key.key == '\t'){ // now need to make sure active widget doesnt want to catch tab 479 if (_activeWidgetIndex == -1) 480 this.cycleActiveWidget(); 481 else{ 482 if (_widgets[_activeWidgetIndex].wantsTab) 483 _widgets[_activeWidgetIndex].keyboardEventCall(key); 484 else 485 this.cycleActiveWidget(); 486 } 487 }else 488 this.cycleActiveWidget(); 489 }else if (_activeWidgetIndex > -1 && (key.key != '\t' || _widgets[_activeWidgetIndex].wantsTab)) 490 _widgets[_activeWidgetIndex].keyboardEventCall(key); 491 } 492 493 /// override initialize to initliaze child widgets 494 override void initialize(){ 495 foreach (widget; _widgets){ 496 widget._display = _display.getSlice(1,1, _position.x, _position.y); // just throw in dummy size/position, resize event will fix that 497 widget.initializeCall(); 498 } 499 } 500 501 /// override timer event to call child widgets' timers 502 override void timerEvent(uinteger msecs){ 503 foreach (widget; _widgets) 504 widget.timerEventCall(msecs); 505 } 506 507 /// override activate event 508 override void activateEvent(bool isActive){ 509 if (isActive){ 510 _activeWidgetIndex = -1; 511 this.cycleActiveWidget(); 512 }else if (_activeWidgetIndex > -1){ 513 _widgets[_activeWidgetIndex].activateEventCall(isActive); 514 } 515 } 516 517 /// called by owner widget to update 518 override void update(){ 519 uinteger space = 0; 520 foreach(i, widget; _widgets){ 521 if (widget.show){ 522 if (_requestingUpdate[i]){ 523 widget._display.cursor = Position(0,0); 524 widget.update(); 525 _requestingUpdate[i] = false; 526 } 527 if (_type == Type.Horizontal){ 528 space += widget.size.width; 529 if (widget.size.height < this._size.height){ 530 foreach (y; widget.size.height .. this._size.height){ 531 _display.cursor = Position(widget._position.x, y); 532 _display.fillLine(' ', _fillColor, _fillColor, widget.size.width); 533 } 534 } 535 }else{ 536 space += widget.size.height; 537 if (widget.size.width < this._size.width){ 538 immutable lineWidth = _size.width - widget.size.width; 539 foreach (y; 0 .. widget.size.height){ 540 _display.cursor = Position(widget.size.width, widget._position.y + y); 541 _display.fillLine(' ', _fillColor, _fillColor, lineWidth); 542 } 543 } 544 } 545 } 546 } 547 if (_type == Type.Horizontal && space < this._size.width){ 548 immutable uinteger lineWidth = this._size.width - space; 549 foreach (y; 0 .. this._size.height){ 550 _display.cursor = Position(space, y); 551 _display.fillLine(' ', _fillColor, _fillColor, lineWidth); 552 } 553 }else if (_type == Type.Vertical && space < this._size.height){ 554 foreach (y; space .. this._size.height){ 555 _display.cursor = Position(0, y); 556 _display.fillLine(' ', _fillColor, _fillColor, this._size.width); 557 } 558 } 559 } 560 /// called to cycle between actveWidgets. This is called by owner widget 561 /// 562 /// Returns: true if cycled to another widget, false if _activeWidgetIndex set to -1 563 override bool cycleActiveWidget(){ 564 // check if need to cycle within current active widget 565 if (_activeWidgetIndex == -1 || !(_widgets[_activeWidgetIndex].cycleActiveWidget())){ 566 integer lastActiveWidgetIndex = _activeWidgetIndex; 567 for (_activeWidgetIndex ++; _activeWidgetIndex < _widgets.length; _activeWidgetIndex ++){ 568 if (_widgets[_activeWidgetIndex].wantsInput && _widgets[_activeWidgetIndex].show) 569 break; 570 } 571 if (_activeWidgetIndex >= _widgets.length) 572 _activeWidgetIndex = -1; 573 574 if (lastActiveWidgetIndex != _activeWidgetIndex){ 575 if (lastActiveWidgetIndex > -1) 576 _widgets[lastActiveWidgetIndex].activateEventCall(false); 577 if (_activeWidgetIndex > -1) 578 _widgets[_activeWidgetIndex].activateEventCall(true); 579 } 580 } 581 return _activeWidgetIndex != -1; 582 } 583 584 /// activate the passed widget if it's in the current layout, return if it was activated or not 585 override bool searchAndActivateWidget(QWidget target){ 586 integer lastActiveWidgetIndex = _activeWidgetIndex; 587 588 // search and activate recursively 589 _activeWidgetIndex = -1; 590 foreach (integer index, widget; _widgets) { 591 if (widget.wantsInput && widget.show && widget.searchAndActivateWidget(target)) { 592 _activeWidgetIndex = index; 593 break; 594 } 595 } 596 597 // and then manipulate the current layout 598 if (lastActiveWidgetIndex != _activeWidgetIndex && lastActiveWidgetIndex > -1) 599 _widgets[lastActiveWidgetIndex].activateEventCall(false); 600 // no need to call activateEvent on new activeWidget, doing `searchAndActivateWidget` in above loop should've done that 601 return _activeWidgetIndex != -1; 602 } 603 604 /// Change the key used for cycling active widgets, changes it for all added widgets as well 605 override void setActiveWidgetCycleKey(dchar newKey){ 606 _activeWidgetCycleKey = newKey; 607 foreach (widget; _widgets) 608 widget.setActiveWidgetCycleKey(newKey); 609 } 610 public: 611 /// Layout type 612 enum Type{ 613 Vertical, 614 Horizontal, 615 } 616 /// constructor 617 this(QLayout.Type type){ 618 _type = type; 619 this._fillColor = DEFAULT_BG; 620 } 621 /// Color for unoccupied space 622 @property Color fillColor(){ 623 return _fillColor; 624 } 625 /// ditto 626 @property Color fillColor(Color newColor){ 627 return _fillColor = newColor; 628 } 629 /// Returns: whether the widget is receiving the Tab key press or not 630 override @property bool wantsTab(){ 631 foreach (widget; _widgets){ 632 if (widget.wantsTab) 633 return true; 634 } 635 return false; 636 } 637 /// Returns: true if the widget wants input 638 override @property bool wantsInput(){ 639 foreach (widget; _widgets){ 640 if (widget.wantsInput) 641 return true; 642 } 643 return false; 644 } 645 /// Returns: true if the cursor should be visible if this widget is active 646 override @property Position cursorPosition(){ 647 // just do a hack, and check only for active widget 648 if (_activeWidgetIndex == -1) 649 return Position(-1, -1); 650 Position cursorPosition = _widgets[_activeWidgetIndex].cursorPosition; 651 if (cursorPosition == Position(-1, -1)) 652 return cursorPosition; 653 return Position(_widgets[_activeWidgetIndex]._position.x + cursorPosition.x, 654 _widgets[_activeWidgetIndex]._position.y + cursorPosition.y); 655 } 656 657 /// adds (appends) a widget to the widgetList, and makes space for it 658 /// 659 /// If there a widget is too large, it's marked as not visible 660 void addWidget(QWidget widget){ 661 widget._parent = this; 662 widget._indexInParent = _widgets.length; 663 widget.setActiveWidgetCycleKey(this._activeWidgetCycleKey); 664 //add it to array 665 _widgets ~= widget; 666 // make space in _requestingUpdate 667 _requestingUpdate ~= true; 668 } 669 /// adds (appends) widgets to the widgetList, and makes space for them 670 /// 671 /// If there a widget is too large, it's marked as not visible 672 void addWidget(QWidget[] widgets){ 673 foreach (i, widget; widgets){ 674 widget._parent = this; 675 widget._indexInParent = _widgets.length+i; 676 widget.setActiveWidgetCycleKey(this._activeWidgetCycleKey); 677 } 678 // add to array 679 _widgets ~= widgets.dup; 680 // make space in _requestingUpdate 681 _requestingUpdate.length += widgets.length; 682 _requestingUpdate[$ - widgets.length .. $] = true; 683 } 684 } 685 686 /// Used to write to display by widgets 687 class Display{ 688 private: 689 /// width & height 690 uinteger _width, _height; 691 /// x and y offsets 692 uinteger _xOff, _yOff; 693 /// cursor position, relative to _xOff and _yOff 694 Position _cursor; 695 /// the terminal 696 TermWrapper _term; 697 /// constructor for when this buffer is just a slice of the actual buffer 698 this(uinteger w, uinteger h, uinteger xOff, uinteger yOff, TermWrapper terminal){ 699 _xOff = xOff; 700 _yOff = yOff; 701 _width = w; 702 _height = h; 703 _term = terminal; 704 } 705 /// Returns: a "slice" of this buffer, that is only limited to some rectangular area 706 /// 707 /// no bound checking is done 708 Display getSlice(uinteger w, uinteger h, uinteger x, uinteger y){ 709 return new Display(w, h, _xOff + x, _yOff + y, _term); 710 } 711 /// modifies an existing Display to act as a "slice" 712 /// 713 /// no bound checking is done 714 void getSlice(Display sliced, uinteger w, uinteger h, uinteger x, uinteger y){ 715 sliced._width = w; 716 sliced._height = h; 717 sliced._xOff = _xOff + x; 718 sliced._yOff = _yOff + y; 719 } 720 public: 721 /// constructor 722 this(uinteger w, uinteger h, TermWrapper terminal){ 723 _width = w; 724 _height = h; 725 _term = terminal; 726 } 727 ~this(){ 728 729 } 730 /// Returns: cursor position 731 @property Position cursor(){ 732 return _cursor; 733 } 734 /// ditto 735 @property Position cursor(Position newPos){ 736 if (newPos.x >= _width) 737 newPos.x = _width-1; 738 if (newPos.y >= _height) 739 newPos.y = _height-1; 740 if (newPos.x < 0) 741 newPos.x = 0; 742 if (newPos.y < 0) 743 newPos.y = 0; 744 return _cursor = newPos; 745 } 746 /// sets background and foreground color 747 void colors(Color fg, Color bg){ 748 _term.color(fg, bg); 749 } 750 /// Writes a line. if string has more characters than there is space for, extra characters will be ignored. 751 /// Each character is written in a 1 cell. 752 void write(dstring str, Color fg, Color bg){ 753 _term.color(fg, bg); 754 this.write(str); 755 } 756 /// ditto 757 void write(dstring str){ 758 foreach (c; str){ 759 if (_cursor.x >= _width){ 760 _cursor.x = 0; 761 _cursor.y ++; 762 } 763 if (_cursor.x < _width && _cursor.y < _height) 764 _term.put(cast(int)(_cursor.x + _xOff), cast(int)(_cursor.y + _yOff), c == '\t' || c == '\n' ? ' ' : c); 765 else 766 break; 767 _cursor.x ++; 768 } 769 } 770 /// fills all remaining cells with a character 771 void fill(dchar c, Color fg, Color bg){ 772 dchar[] line; 773 line.length = _width; 774 line[] = c; 775 _term.color(fg, bg); 776 while (_cursor.y < _height){ 777 _term.write(cast(int)(_cursor.x + _xOff), cast(int)(_cursor.y + _yOff), cast(dstring)line[0 .. _width - _cursor.x]); 778 _cursor.y ++; 779 _cursor.x = 0; 780 } 781 } 782 /// fills rest of current line with a character 783 void fillLine(dchar c, Color fg, Color bg, uinteger max = 0){ 784 dchar[] line; 785 line.length = max < _width - _cursor.x && max > 0 ? max : _width - _cursor.x; 786 line[] = c; 787 _term.color(fg, bg); 788 _term.write(cast(int)(_cursor.x + _xOff), cast(int)(_cursor.y + _yOff), cast(dstring)line); 789 _cursor.x += line.length; 790 if (_cursor.x >= _width -1){ 791 _cursor.y ++; 792 _cursor.x = 0; 793 } 794 } 795 } 796 797 /// A terminal (as the name says). 798 class QTerminal : QLayout{ 799 private: 800 /// To actually access the terminal 801 TermWrapper _termWrap; 802 /// Whether to call resize before next update 803 bool _requestingResize; 804 /// set to false to stop UI loop in run() 805 bool _isRunning; 806 807 /// Reads InputEvent and calls appropriate functions to address those events 808 void readEvent(Event event){ 809 if (event.type == Event.Type.HangupInterrupt){ 810 _isRunning = false; 811 }else if (event.type == Event.Type.Keyboard){ 812 KeyboardEvent kPress = event.keyboard; 813 this.keyboardEventCall(kPress); 814 }else if (event.type == Event.Type.Mouse){ 815 this.mouseEventCall(event.mouse); 816 }else if (event.type == Event.Type.Resize){ 817 //update self size 818 _size.height = event.resize.height; 819 _size.width = event.resize.width; 820 _display._height = _size.height; 821 _display._width = _size.width; 822 //call size change on all widgets 823 resizeEventCall(); 824 } 825 } 826 827 protected: 828 829 override void resizeEvent(){ 830 _requestingResize = false; 831 super.resizeEvent(); 832 } 833 834 override void update(){ 835 // resize if needed 836 if (_requestingResize) 837 this.resizeEventCall(); 838 super.update(); 839 // check if need to show/hide cursor 840 Position cursorPos = this.cursorPosition; 841 if (cursorPos == Position(-1, -1)){ 842 _termWrap.cursorVisible = false; 843 }else{ 844 _termWrap.moveCursor(cast(int)cursorPos.x, cast(int)cursorPos.y); 845 _termWrap.cursorVisible = true; 846 } 847 _termWrap.flush; 848 } 849 850 public: 851 /// time to wait between timer events (milliseconds) 852 ushort timerMsecs; 853 /// constructor 854 this(QLayout.Type displayType = QLayout.Type.Vertical, ushort timerDuration = 500){ 855 super(displayType); 856 timerMsecs = timerDuration; 857 858 _termWrap = new TermWrapper(); 859 _display = new Display(1,1, _termWrap); 860 this._isActive = true; // so it can make other widgets active on mouse events 861 } 862 ~this(){ 863 .destroy(_termWrap); 864 .destroy(_display); 865 } 866 867 /// Called to make container widgets (QLayouts) recalcualte widgets' sizes before update; 868 override void requestResize(){ 869 _requestingResize = true; 870 } 871 872 /// stops UI loop. **not instant**, if it is in-between updates, event functions, or timers, it will complete those first 873 void terminate(){ 874 _isRunning = false; 875 } 876 877 /// starts the UI loop 878 void run(){ 879 // set size 880 _size.width = _termWrap.width(); 881 _size.height = _termWrap.height(); 882 _display._width = _size.width; 883 _display._height = _size.height; 884 //ready 885 initializeCall(); 886 resizeEventCall(); 887 //draw the whole thing 888 update(); 889 _isRunning = true; 890 // the stop watch, to count how much time has passed after each timerEvent 891 StopWatch sw = StopWatch(AutoStart.yes); 892 while (_isRunning){ 893 int timeout = cast(int)(timerMsecs - sw.peek.total!"msecs"); 894 Event event; 895 while (_termWrap.getEvent(timeout, event) > 0){ 896 readEvent(event); 897 timeout = cast(int)(timerMsecs - sw.peek.total!"msecs"); 898 update(); 899 } 900 if (sw.peek.total!"msecs" >= timerMsecs){ 901 this.timerEventCall(sw.peek.total!"msecs"); 902 sw.reset; 903 sw.start; 904 update(); 905 } 906 } 907 } 908 909 /// search the passed widget recursively and activate it, returns if the activation was successful 910 bool activateWidget(QWidget target) { 911 return this.searchAndActivateWidget(target); 912 } 913 914 /// Changes the key used to cycle between active widgets. 915 /// 916 /// for `key`, use either ASCII value of keyboard key, or use KeyboardEvent.Key or KeyboardEvent.CtrlKeys 917 override void setActiveWidgetCycleKey(dchar key){ 918 super.setActiveWidgetCycleKey(key); 919 } 920 }