1 /++ 2 Some widgets that are included in the package. 3 +/ 4 module qui.widgets; 5 6 import qui.qui; 7 import qui.utils; 8 import utils.misc; 9 import utils.lists; 10 11 /// Displays some text 12 /// 13 /// And it can't handle new-line characters 14 /// 15 /// If the text doesn't fit in width, it will move left-right 16 class TextLabelWidget : QWidget{ 17 private: 18 /// number of chars not displayed on left 19 uinteger xOffset = 0; 20 /// max xOffset 21 uinteger maxXOffset; 22 /// the text to display 23 dstring _caption; 24 /// if in the last timerEvent, xOffset was increased 25 bool increasedXOffset = true; 26 27 /// calculates the maxXOffset, and changes xOffset if it's above it 28 void calculateMaxXOffset(){ 29 if (_caption.length <= size.width){ 30 maxXOffset = 0; 31 xOffset = 0; 32 }else{ 33 maxXOffset = _caption.length - _size.width; 34 if (xOffset > maxXOffset) 35 xOffset = maxXOffset; 36 } 37 } 38 protected: 39 override void timerEvent(uinteger msecs){ 40 static uinteger accumulatedTime; 41 if (maxXOffset > 0){ 42 accumulatedTime += msecs; 43 if (xOffset >= maxXOffset) 44 increasedXOffset = false; 45 else if (xOffset == 0) 46 increasedXOffset = true; 47 while (accumulatedTime >= scrollTimer){ 48 accumulatedTime -= scrollTimer; 49 if (increasedXOffset){ 50 if (xOffset < maxXOffset) 51 xOffset ++; 52 }else if (xOffset > 0) 53 xOffset --; 54 // request update 55 requestUpdate(); 56 } 57 } 58 } 59 60 override void resizeEvent(){ 61 calculateMaxXOffset; 62 requestUpdate(); 63 } 64 65 override void update(){ 66 _display.write(_caption.scrollHorizontal(xOffset, _size.width), textColor, backgroundColor); 67 } 68 69 public: 70 /// text and background colors 71 Color textColor, backgroundColor; 72 /// milliseconds after it scrolls 1 pixel, in case text too long to fit in 1 line 73 uinteger scrollTimer; 74 /// constructor 75 this(dstring newCaption = ""){ 76 this.caption = newCaption; 77 textColor = DEFAULT_FG; 78 backgroundColor = DEFAULT_BG; 79 this._size.maxHeight = 1; 80 scrollTimer = 500; 81 } 82 83 /// the text to display 84 @property dstring caption(){ 85 return _caption; 86 } 87 /// ditto 88 @property dstring caption(dstring newCaption){ 89 _caption = newCaption; 90 calculateMaxXOffset; 91 // request update 92 requestUpdate(); 93 return _caption; 94 } 95 } 96 97 /// Displays a left-to-right progress bar. 98 /// 99 /// Can also display text (just like TextLabelWidget) 100 class ProgressbarWidget : TextLabelWidget{ 101 private: 102 uinteger _max, _progress; 103 protected: 104 override void update(){ 105 // if caption fits in width, center align it 106 dstring text; 107 if (_caption.length < _size.width) 108 text = centerAlignText(_caption, _size.width); 109 else 110 text = _caption.scrollHorizontal(xOffset, _size.width); 111 // number of chars to be colored in barColor 112 uinteger fillCharCount = (_progress * _size.width) / _max; 113 // line number on which the caption will be written 114 for (uinteger i = 0,captionLineNumber = this._size.height / 2; i < this._size.height; i ++){ 115 _display.cursor = Position(0, i); 116 if (i == captionLineNumber){ 117 _display.write(cast(dchar[])text[0 .. fillCharCount], backgroundColor, barColor); 118 _display.write(cast(dchar[])text[fillCharCount .. text.length], barColor, backgroundColor); 119 }else{ 120 if (fillCharCount) 121 _display.fillLine(' ', backgroundColor, barColor, fillCharCount); 122 if (fillCharCount < this._size.width) 123 _display.fillLine(' ', barColor, backgroundColor); 124 } 125 } 126 // write till _progress 127 _display.write(cast(dchar[])text[0 .. fillCharCount], backgroundColor, barColor); 128 // write the empty bar 129 _display.write(cast(dchar[])text[fillCharCount .. text.length], barColor, backgroundColor); 130 } 131 public: 132 /// background color, and bar's color 133 Color backgroundColor, barColor; 134 /// constructor 135 this(uinteger max = 100, uinteger progress = 0){ 136 _caption = null; 137 this.max = max; 138 this.progress = progress; 139 // no max height limit on this one 140 this._size.maxHeight = 0; 141 142 barColor = DEFAULT_FG; 143 backgroundColor = DEFAULT_BG; 144 } 145 /// The 'total', or the max-progress. getter 146 @property uinteger max(){ 147 return _max; 148 } 149 /// The 'total' or the max-progress. setter 150 @property uinteger max(uinteger newMax){ 151 _max = newMax; 152 requestUpdate(); 153 return _max; 154 } 155 /// the amount of progress. getter 156 @property uinteger progress(){ 157 return _progress; 158 } 159 /// the amount of progress. setter 160 @property uinteger progress(uinteger newProgress){ 161 _progress = newProgress; 162 requestUpdate(); 163 return _progress; 164 } 165 } 166 167 /// To get single-line input from keyboard 168 class EditLineWidget : QWidget{ 169 private: 170 /// text that's been input-ed 171 dchar[] _text; 172 /// position of cursor 173 uinteger _x; 174 /// how many chars wont be displayed on left 175 uinteger _scrollX; 176 177 /// called to fix _scrollX and _x when input is changed or _x is changed 178 void reScroll(){ 179 adjustScrollingOffset(_x, _size.width, _text.length, _scrollX); 180 } 181 protected: 182 /// override resize to re-scroll 183 override void resizeEvent(){ 184 requestUpdate(); 185 reScroll; 186 } 187 override void mouseEvent(MouseEvent mouse){ 188 if (mouse.button == MouseEvent.Button.Left){ 189 _x = mouse.x + _scrollX; 190 } 191 requestUpdate(); 192 reScroll; 193 } 194 override void keyboardEvent(KeyboardEvent key){ 195 if (key.isChar){ 196 //insert that key 197 if (key.key == '\b'){ 198 //backspace 199 if (_x > 0){ 200 if (_x == _text.length){ 201 _text.length --; 202 }else{ 203 _text = _text.deleteElement(_x-1); 204 } 205 _x --; 206 } 207 }else if (key.key != '\n'){ 208 if (_x == _text.length){ 209 //insert at end 210 _text ~= cast(dchar)key.key; 211 }else{ 212 _text = _text.insertElement([cast(dchar)key.key], _x); 213 } 214 _x ++; 215 } 216 }else{ 217 if (key.key == Key.LeftArrow && _x > 0){ 218 _x --; 219 }else if (key.key == Key.RightArrow && _x < _text.length){ 220 _x ++; 221 }else if (key.key == Key.Delete && _x < _text.length){ 222 _text = _text.deleteElement(_x); 223 } 224 } 225 requestUpdate(); 226 reScroll; 227 } 228 override void update(){ 229 _display.write(cast(dstring)this._text.scrollHorizontal(cast(integer)_scrollX, _size.width),textColor,backgroundColor); 230 } 231 public: 232 /// background, text, caption, and caption's background colors 233 Color backgroundColor, textColor; 234 /// constructor 235 this(dstring text = ""){ 236 this._text = cast(dchar[])text.dup; 237 //specify min/max 238 _size.minHeight = 1; 239 _size.maxHeight = 1; 240 // don't want tab key by default 241 _wantsTab = false; 242 // and input too, obvious 243 _wantsInput = true; 244 245 textColor = DEFAULT_FG; 246 backgroundColor = DEFAULT_BG; 247 } 248 249 ///The text that has been input-ed. 250 @property dstring text(){ 251 return cast(dstring)_text.dup; 252 } 253 ///The text that has been input-ed. 254 @property dstring text(dstring newText){ 255 _text = cast(dchar[])newText.dup; 256 // request update 257 requestUpdate(); 258 return cast(dstring)newText; 259 } 260 // override cursor position 261 override @property Position cursorPosition(){ 262 return Position(_x - _scrollX, 0); 263 } 264 } 265 266 /// Can be used as a simple text editor, or to just display text 267 class MemoWidget : QWidget{ 268 private: 269 List!dstring _lines; 270 /// how many characters/lines are skipped 271 uinteger _scrollX, _scrollY; 272 /// whether the cursor is, relative to line#0 character#0 273 uinteger _cursorX, _cursorY; 274 /// whether the text in it will be editable 275 bool _enableEditing = true; 276 /// used by widget itself to recalculate scrolling 277 void reScroll(){ 278 // _scrollY 279 adjustScrollingOffset(_cursorY, this._size.height, lineCount, _scrollY); 280 // _scrollX 281 adjustScrollingOffset(_cursorX, this._size.width, readLine(_cursorY).length, _scrollX); 282 } 283 /// used by widget itself to move cursor 284 void moveCursor(uinteger x, uinteger y){ 285 _cursorX = x; 286 _cursorY = y; 287 288 if (_cursorY > lineCount) 289 _cursorY = lineCount-1; 290 if (_cursorX > readLine(_cursorY).length) 291 _cursorX = readLine(_cursorY).length; 292 } 293 /// Reads a line from widgetLines 294 dstring readLine(uinteger index){ 295 if (index >= _lines.length) 296 return ""; 297 return _lines.read(index); 298 } 299 /// overwrites a line 300 void overwriteLine(uinteger index, dstring line){ 301 if (index == _lines.length) 302 _lines.append(line); 303 else 304 _lines.set(index,line); 305 306 } 307 /// deletes a line 308 void removeLine(uinteger index){ 309 if (index < _lines.length) 310 _lines.remove(index); 311 } 312 /// inserts a line 313 void insertLine(uinteger index, dstring line){ 314 if (index == _lines.length) 315 _lines.append(line); 316 else 317 _lines.insert(index, line); 318 } 319 /// adds a line 320 void addLine(dstring line){ 321 _lines.append(line); 322 } 323 /// returns lines count 324 @property uinteger lineCount(){ 325 return _lines.length+1; 326 } 327 protected: 328 override void update(){ 329 const uinteger count = lineCount; 330 if (count > 0){ 331 //write lines to memo 332 for (uinteger i = _scrollY; i < count && _display.cursor.y < _size.height; i++){ 333 _display.write(readLine(i).scrollHorizontal(_scrollX, this._size.width), 334 textColor, backgroundColor); 335 } 336 } 337 _display.fill(' ', textColor, backgroundColor); 338 } 339 340 override void resizeEvent(){ 341 requestUpdate(); 342 reScroll(); 343 } 344 345 override void mouseEvent(MouseEvent mouse){ 346 //calculate mouse position, relative to scroll 347 mouse.x = mouse.x + cast(int)_scrollX; 348 mouse.y = mouse.y + cast(int)_scrollY; 349 if (mouse.button == mouse.Button.Left){ 350 requestUpdate(); 351 moveCursor(mouse.x, mouse.y); 352 }else if (mouse.button == mouse.Button.ScrollDown){ 353 if (_cursorY+1 < lineCount){ 354 requestUpdate(); 355 moveCursor(_cursorX, _cursorY + 4); 356 reScroll(); 357 } 358 }else if (mouse.button == mouse.Button.ScrollUp){ 359 if (_cursorY > 0){ 360 requestUpdate(); 361 if (_cursorY < 4){ 362 moveCursor(_cursorX, 0); 363 }else{ 364 moveCursor(_cursorX, _cursorY - 4); 365 } 366 reScroll(); 367 } 368 } 369 } 370 // too big of a mess to be dealt with right now, TODO try to make this shorter 371 override void keyboardEvent(KeyboardEvent key){ 372 if (key.isChar){ 373 if (_enableEditing){ 374 requestUpdate(); 375 dstring currentLine = readLine(_cursorY); 376 //check if backspace 377 if (key.key == '\b'){ 378 //make sure that it's not the first line, first line cannot be removed 379 if (_cursorY > 0){ 380 //check if has to remove a '\n' 381 if (_cursorX == 0){ 382 _cursorY --; 383 //if line's not empty, append it to previous line 384 _cursorX = readLine(_cursorY).length; 385 if (currentLine != ""){ 386 //else, append this line to previous 387 overwriteLine(_cursorY, readLine(_cursorY)~currentLine); 388 } 389 removeLine(_cursorY+1); 390 }else{ 391 overwriteLine(_cursorY, cast(dstring)deleteElement(cast(dchar[])currentLine,_cursorX-1)); 392 _cursorX --; 393 } 394 }else if (_cursorX > 0){ 395 overwriteLine(_cursorY, cast(dstring)deleteElement(cast(dchar[])currentLine,_cursorX-1)); 396 _cursorX --; 397 } 398 399 }else if (key.key == '\n'){ 400 //insert a newline 401 if (_cursorX == readLine(_cursorY).length){ 402 if (_cursorY >= lineCount - 1){ 403 _lines.append(""); 404 }else{ 405 insertLine(_cursorY + 1,""); 406 } 407 }else{ 408 dstring[2] line; 409 line[0] = readLine(_cursorY); 410 line[1] = line[0][_cursorX .. line[0].length]; 411 line[0] = line[0][0 .. _cursorX]; 412 overwriteLine(_cursorY, line[0]); 413 if (_cursorY >= lineCount - 1){ 414 _lines.append(line[1]); 415 }else{ 416 insertLine(_cursorY + 1, line[1]); 417 } 418 } 419 _cursorY ++; 420 _cursorX = 0; 421 }else if (key.key == '\t'){ 422 //convert it to 4 spaces 423 overwriteLine(_cursorY, cast(dstring)insertElement(cast(dchar[])currentLine,cast(dchar[])" ",_cursorX)); 424 _cursorX += 4; 425 }else{ 426 //insert that char 427 overwriteLine(_cursorY, cast(dstring)insertElement(cast(dchar[])currentLine,[cast(dchar)key.key],_cursorX)); 428 _cursorX ++; 429 } 430 } 431 }else{ 432 if (key.key == Key.Delete && _enableEditing){ 433 requestUpdate(); 434 //check if is deleting \n 435 if (_cursorX == readLine(_cursorY).length && _cursorY+1 < lineCount){ 436 //merge next line with this one 437 dchar[] line = cast(dchar[])readLine(_cursorY)~readLine(_cursorY+1); 438 overwriteLine(_cursorY, cast(dstring)line); 439 //remove next line 440 removeLine(_cursorY+1); 441 }else if (_cursorX < readLine(_cursorY).length){ 442 dchar[] line = cast(dchar[])readLine(_cursorY); 443 line = line.deleteElement(_cursorX); 444 overwriteLine(_cursorY, cast(dstring)line); 445 } 446 }else if (key.key == Key.DownArrow){ 447 if (_cursorY+1 < lineCount){ 448 requestUpdate(); 449 _cursorY ++; 450 } 451 }else if (key.key == Key.UpArrow){ 452 if (_cursorY > 0){ 453 requestUpdate(); 454 _cursorY --; 455 } 456 }else if (key.key == Key.LeftArrow){ 457 if ((_cursorY >= 0 && _cursorX > 0) || (_cursorY > 0 && _cursorX == 0)){ 458 requestUpdate(); 459 if (_cursorX == 0){ 460 _cursorY --; 461 _cursorX = readLine(_cursorY).length; 462 }else{ 463 _cursorX --; 464 } 465 } 466 }else if (key.key == Key.RightArrow){ 467 requestUpdate(); 468 if (_cursorX == readLine(_cursorY).length){ 469 if (_cursorY+1 < lineCount){ 470 _cursorX = 0; 471 _cursorY ++; 472 _scrollX = 0; 473 } 474 }else{ 475 _cursorX ++; 476 } 477 } 478 } 479 // I'll use this this time not to move the cursor, but to fix the cursor position 480 moveCursor(_cursorX,_cursorY); 481 reScroll(); 482 } 483 public: 484 /// background and text colors 485 Color backgroundColor, textColor; 486 /// constructor 487 this(bool allowEditing = true){ 488 _lines = new List!dstring; 489 _scrollX = 0; 490 _scrollY = 0; 491 _cursorX = 0; 492 _cursorY = 0; 493 this.editable = allowEditing; 494 495 textColor = DEFAULT_FG; 496 backgroundColor = DEFAULT_BG; 497 } 498 ~this(){ 499 .destroy(_lines); 500 } 501 502 ///returns a list of lines in memo 503 /// 504 ///To modify the content, just modify it in the returned list 505 /// 506 ///class `List` is defined in `utils.lists.d` 507 @property List!dstring lines(){ 508 return _lines; 509 } 510 ///Returns true if memo's contents cannot be modified, by user 511 @property bool editable(){ 512 return _enableEditing; 513 } 514 ///sets whether to allow modifying of contents (false) or not (true) 515 @property bool editable(bool newPermission){ 516 _wantsTab = newPermission; 517 _wantsInput = newPermission; 518 return _enableEditing = newPermission; 519 } 520 /// override cursor position 521 override @property Position cursorPosition(){ 522 return _enableEditing ? Position(_cursorX - _scrollX, _cursorY - _scrollY) : Position(-1,-1); 523 } 524 } 525 526 /// Displays an un-scrollable log 527 /// 528 /// It's content cannot be modified by user, like a MemoWidget with editing disabled, but automatically scrolls down as new lines 529 /// are added, and it wraps long lines. 530 class LogWidget : QWidget{ 531 private: 532 /// stores the logs 533 List!dstring _logs; 534 /// The index in _logs where the oldest added line is 535 uinteger _startIndex; 536 /// the maximum number of lines to store 537 uinteger _maxLines; 538 /// Returns: line at an index 539 dstring getLine(uinteger index){ 540 if (index >= _logs.length) 541 return ""; 542 return _logs.read((index + _startIndex) % _maxLines); 543 } 544 /// wrap a line 545 dstring[] wrapLine(dstring str){ 546 dstring[] r; 547 str = str.dup; 548 while (str.length > 0){ 549 r ~= _size.width > str.length ? str : str[0 .. _size.width]; 550 str = _size.width < str.length ? str[_size.width .. $] : []; 551 } 552 return r; 553 } 554 protected: 555 override void update(){ 556 _display.colors(textColor, backgroundColor); 557 integer lastY = _size.height; 558 for (integer i = _logs.length-1; i >= 0; i --){ 559 dstring line = getLine(i); 560 dstring[] wrappedLine = wrapLine(line); 561 if (wrappedLine.length == 0) 562 continue; 563 if (lastY < wrappedLine.length) 564 wrappedLine = wrappedLine[wrappedLine.length - lastY .. $]; 565 immutable integer startY = lastY - wrappedLine.length; 566 foreach (lineno, currentLine; wrappedLine){ 567 _display.cursor = Position(0, lineno + startY); 568 _display.write(currentLine); 569 if (currentLine.length < _size.width) 570 _display.fillLine(' ', textColor, backgroundColor); 571 } 572 lastY = startY; 573 } 574 _display.cursor = Position(0, 0); 575 foreach (y; 0 .. lastY) 576 _display.fillLine(' ', textColor, backgroundColor); 577 } 578 579 override void resizeEvent() { 580 requestUpdate(); 581 } 582 public: 583 /// background and text color 584 Color backgroundColor, textColor; 585 /// constructor 586 this(uinteger maxLen=100){ 587 _maxLines = maxLen; 588 _logs = new List!dstring; 589 _startIndex = 0; 590 591 textColor = DEFAULT_FG; 592 backgroundColor = DEFAULT_BG; 593 } 594 ~this(){ 595 _logs.destroy; 596 } 597 598 ///adds string to the log, and scrolls down to it. 599 /// newline character is not allowed 600 void add(dstring item){ 601 //check if needs to overwrite 602 if (_logs.length > _maxLines){ 603 _startIndex = (_startIndex + 1) % _maxLines; 604 _logs.set(_startIndex, item); 605 }else 606 _logs.append(item); 607 requestUpdate(); 608 } 609 ///clears the log 610 void clear(){ 611 _logs.clear; 612 requestUpdate(); 613 } 614 } 615 616 /// Just occupies some space. Use this to put space between widgets 617 /// 618 /// To specify the size, use the minHeight, maxHeight, minWidth, and maxWidth. only specifying the width and/or height will have no effect 619 class SplitterWidget : QWidget{ 620 protected: 621 override void resizeEvent() { 622 requestUpdate(); 623 } 624 625 override void update(){ 626 _display.fill(' ',DEFAULT_FG, color); 627 } 628 public: 629 /// color of this widget 630 Color color; 631 /// constructor 632 this(){ 633 this.color = DEFAULT_BG; 634 } 635 }