1 /++ 2 This module contains most of the functions you'll need. 3 All the 'base' classes, like QWidget are defined in this. 4 There are some classes, like List, that are defined in 5 other modules. 6 +/ 7 module qui.qui; 8 9 import qui.misc; 10 import qui.lists; 11 import qui.baseconv;//used for hexadecimal colors 12 import std.stdio;//used by QTheme.themeToFile 13 import arsd.terminal; 14 15 ///Mouse Click, or Ms Wheel scroll event 16 /// 17 ///The mouseEvent function is called with this. 18 struct MouseClick{ 19 ///Types of buttons 20 enum Button{ 21 Left,/// Left mouse button 22 ScrollUp,/// MS wheel was scrolled up 23 ScrollDown,/// MS wheel was scrolled down 24 Right,/// Right mouse button was pressed 25 } 26 ///Stores which button was pressed 27 Button mouseButton; 28 /// the x-axis of mouse cursor, 0 means left-most 29 uinteger x; 30 /// the y-axis of mouse cursor, 0 means top-most 31 uinteger y; 32 } 33 34 ///Key press event, keyboardEvent function is called with this 35 /// 36 ///A note: backspace (`\b`) and enter (`\n`) are not included in KeyPress.NonCharKey 37 struct KeyPress{ 38 dchar key;/// stores which key was pressed 39 40 /// Returns true if the key was a character. 41 /// 42 /// A note: backspace (`\b`) and enter (`\n`) are not included in KeyPress.NonCharKey 43 bool isChar(){ 44 return !(key >= NonCharKey.min && key <= NonCharKey.max); 45 } 46 /// Types of non-character keys 47 enum NonCharKey{ 48 escape = 0x1b + 0xF0000, 49 F1 = 0x70 + 0xF0000, 50 F2 = 0x71 + 0xF0000, 51 F3 = 0x72 + 0xF0000, 52 F4 = 0x73 + 0xF0000, 53 F5 = 0x74 + 0xF0000, 54 F6 = 0x75 + 0xF0000, 55 F7 = 0x76 + 0xF0000, 56 F8 = 0x77 + 0xF0000, 57 F9 = 0x78 + 0xF0000, 58 F10 = 0x79 + 0xF0000, 59 F11 = 0x7A + 0xF0000, 60 F12 = 0x7B + 0xF0000, 61 LeftArrow = 0x25 + 0xF0000, 62 RightArrow = 0x27 + 0xF0000, 63 UpArrow = 0x26 + 0xF0000, 64 DownArrow = 0x28 + 0xF0000, 65 Insert = 0x2d + 0xF0000, 66 Delete = 0x2e + 0xF0000, 67 Home = 0x24 + 0xF0000, 68 End = 0x23 + 0xF0000, 69 PageUp = 0x21 + 0xF0000, 70 PageDown = 0x22 + 0xF0000, 71 } 72 } 73 74 /// A 24 bit, RGB, color 75 /// 76 /// `r` represents amount of red, `g` is green, and `b` is blue. 77 /// the `a` is ignored 78 alias RGBColor = RGB; 79 80 /// Used to store position for widgets 81 struct Position{ 82 uinteger x, y; 83 } 84 85 /// To store size for widgets 86 /// 87 /// zero in min/max means no limit 88 struct Size{ 89 uinteger width, height; 90 uinteger minHeight = 0, minWidth = 0; 91 uinteger maxHeight = 0, maxWidth = 0; 92 } 93 94 /// The whole terminal is divided into cells, total number of cells = length * height of terminal 95 /// 96 /// Each cell has it's own character, foreground color, and background color 97 struct Cell{ 98 char c; 99 RGBColor textColor; 100 RGBColor bgColor; 101 } 102 103 /// mouseEvent function 104 alias MouseEventFuction = void delegate(MouseClick); 105 ///keyboardEvent function 106 alias KeyboardEventFunction = void delegate(KeyPress); 107 108 109 /// Base class for all widgets, including layouts and QTerminal 110 /// 111 /// Use this as parent-class for new widgets 112 abstract class QWidget{ 113 protected: 114 ///specifies position of this widget 115 Position widgetPosition; 116 ///size of this widget 117 Size widgetSize; 118 ///caption of this widget, it's up to the widget how to use this, progressbarWidget shows this inside the bar... 119 string widgetCaption; 120 ///whether this widget should be drawn or not 121 bool widgetShow = true; 122 ///to specify if this widget needs to be updated or not, mark this as true when the widget has changed 123 bool needsUpdate = true; 124 ///specifies name of this widget. must be unique, as it is used to identify widgets in theme 125 string widgetName = null; 126 /// specifies that how much height (in horizontal layout) or width (in vertical) is given to this widget. 127 /// The ratio of all widgets is added up and height/width for each widget is then calculated using this 128 uinteger widgetSizeRatio = 1; 129 130 ///The theme that is currently used 131 /// 132 ///The widget is free to modify the theme 133 QTheme widgetTheme; 134 135 /// Called by widget when a redraw is needed, but no redraw is scheduled 136 /// 137 /// In other words: call this function using: 138 /// ``` 139 /// if (forceUpdate !is null){ 140 /// forceUpdate(); 141 /// } 142 /// ``` 143 /// when an update is needed, but it's not sure if an update will be called. 144 /// Update is automatically called after mouseEvent and keyboardEvent 145 bool delegate() forceUpdate; 146 147 /// Called by widgets (usually keyboard-input-taking) to position the cursor 148 /// 149 /// It can only be called if the widget is active (i.e selected), in non-active widgets, it's null; 150 void delegate(uinteger x, uinteger y) cursorPos; 151 152 /// custom mouse event, if not null, it should be called before doing anything else in mouseEvent. 153 /// 154 /// Like: 155 /// ``` 156 /// override void mouseEvent(MouseClick mouse){ 157 /// super.mouseEvent(mouse); 158 /// // rest of the code here 159 /// } 160 /// ``` 161 MouseEventFuction customMouseEvent; 162 163 /// custom keyboard event, if not null, it should be called before doing anything else in keyboardEvent. 164 /// 165 /// Like: 166 /// ``` 167 /// override void keyboardEvent(KeyPress key){ 168 /// super.keyboardEvent(key); 169 /// // rest of the code here 170 /// } 171 /// ``` 172 KeyboardEventFunction customKeyboardEvent; 173 public: 174 /// Called by owner when mouse is clicked with cursor on this widget. 175 /// 176 /// `forceUpdate` is not required after this 177 void mouseEvent(MouseClick mouse){ 178 if (customMouseEvent !is null){ 179 customMouseEvent(mouse); 180 } 181 } 182 183 /// Called by owner when key is pressed and this widget is active. 184 /// 185 /// `forceUpdate` is not required after this 186 void keyboardEvent(KeyPress key){ 187 if (customKeyboardEvent !is null){ 188 customKeyboardEvent(key); 189 } 190 } 191 192 /// Called by owner to update. 193 /// 194 /// Return false if no need to update, and true if an update is required, and the new display in `display` Matrix 195 abstract bool update(ref Matrix display);///return true to indicate that it has to be redrawn, else, make changes in display 196 197 /// Called by owner to indicate that widget has to 're-fetch' colors from the theme. 198 abstract void updateColors(); 199 200 //event properties 201 /// use to change the custom mouse event 202 @property MouseEventFuction onMouseEvent(MouseEventFuction func){ 203 return customMouseEvent = func; 204 } 205 /// use to change the custom keyboard event 206 @property KeyboardEventFunction onKeyboardEvent(KeyboardEventFunction func){ 207 return customKeyboardEvent = func; 208 } 209 210 211 //properties: 212 213 /// The name of the widget. Read-only, cannot be modified 214 @property string name(){ 215 return widgetName; 216 } 217 218 /// caption of the widget. setter 219 @property string caption(){ 220 return widgetCaption; 221 } 222 /// caption of the widget. getter 223 @property string caption(string newCaption){ 224 needsUpdate = true; 225 widgetCaption = newCaption; 226 if (forceUpdate !is null){ 227 forceUpdate(); 228 } 229 return widgetCaption; 230 } 231 232 /// position of the widget. getter 233 @property Position position(){ 234 return widgetPosition; 235 } 236 /// position of the widget. setter 237 @property Position position(Position newPosition){ 238 widgetPosition = newPosition; 239 if (forceUpdate !is null){ 240 forceUpdate(); 241 } 242 return widgetPosition; 243 } 244 245 /// size (width/height) of the widget. getter 246 @property uinteger sizeRatio(){ 247 return widgetSizeRatio; 248 } 249 /// size (width/height) of the widget. setter 250 @property uinteger sizeRatio(uinteger newRatio){ 251 needsUpdate = true; 252 widgetSizeRatio = newRatio; 253 if (forceUpdate !is null){ 254 forceUpdate(); 255 } 256 return widgetSizeRatio; 257 } 258 259 /// visibility of the widget. getter 260 @property bool visible(){ 261 return widgetShow; 262 } 263 /// visibility of the widget. setter 264 @property bool visible(bool visibility){ 265 needsUpdate = true; 266 widgetShow = visibility; 267 if (forceUpdate !is null){ 268 forceUpdate(); 269 } 270 return widgetShow; 271 } 272 273 /// theme of the widget. getter 274 @property QTheme theme(){ 275 return widgetTheme; 276 } 277 /// theme of the widget. setter 278 @property QTheme theme(QTheme newTheme){ 279 needsUpdate = true; 280 widgetTheme = newTheme; 281 if (forceUpdate !is null){ 282 forceUpdate(); 283 } 284 return widgetTheme; 285 } 286 287 /// called by owner to set the `forceUpdate` function, which is used to force an update immediately. 288 @property bool delegate() onForceUpdate(bool delegate() newOnForceUpdate){ 289 return forceUpdate = newOnForceUpdate; 290 } 291 /// called by the owner to set the `cursorPos` function, which is used to position the cursor on the terminal. 292 @property void delegate(uinteger, uinteger) onCursorPosition(void delegate(uinteger, uinteger) newOnCursorPos){ 293 return cursorPos = newOnCursorPos; 294 } 295 /// size of the widget. getter 296 @property Size size(){ 297 return widgetSize; 298 } 299 /// size of the widget. setter 300 @property Size size(Size newSize){ 301 //check if height or width < min 302 if (newSize.minWidth > 0 && newSize.width < newSize.minWidth){ 303 return widgetSize; 304 }else if (newSize.maxWidth > 0 && newSize.width > newSize.maxWidth){ 305 return widgetSize; 306 }else if (newSize.minHeight > 0 && newSize.height < newSize.minHeight){ 307 return widgetSize; 308 }else if (newSize.maxHeight > 0 && newSize.height > newSize.maxHeight){ 309 return widgetSize; 310 }else{ 311 needsUpdate = true; 312 widgetSize = newSize; 313 if (forceUpdate !is null){ 314 forceUpdate(); 315 } 316 return widgetSize; 317 } 318 } 319 } 320 321 ///Used to place widgets in an order (i.e vertical or horizontal) 322 /// 323 ///The QTerminal is also a layout, basically. 324 /// 325 ///Name in theme: 'layout'; 326 class QLayout : QWidget{ 327 private: 328 // array of all the widgets that have been added to this layout 329 QWidget[] widgetList; 330 // contains reference to the active widget, null if no active widget 331 QWidget activeWidget; 332 // stores the layout type, horizontal or vertical 333 LayoutDisplayType layoutType; 334 335 // background color 336 RGBColor backColor; 337 // foreground color 338 RGBColor foreColor; 339 Cell emptySpace; 340 // stores whether an update is in progress 341 bool isUpdating = false; 342 343 // recalculates the size and position of every widget inside layout 344 void recalculateWidgetsSize(){ 345 uinteger ratioTotal = 0; 346 Size newSize; 347 //calculate total ratio 348 foreach (currentWidget; widgetList){ 349 ratioTotal += currentWidget.sizeRatio; 350 } 351 Position newPosition; 352 uinteger newWidth = 0, newHeight = 0; 353 uinteger availableWidth = widgetSize.width; 354 uinteger availableHeight = widgetSize.height; 355 356 if (layoutType == LayoutDisplayType.Vertical){ 357 //make space for new widget 358 foreach(w; widgetList){ 359 //if a widget is not visible, skip it 360 if (w.visible){ 361 //recalculate position 362 newPosition.x = widgetPosition.x;//x axis is always same, cause this is a vertical (not horizontal) layout 363 if (newHeight > 0){ 364 newPosition.y += newHeight;//add previous widget's height to get new y axis position 365 }else{ 366 newPosition.y = 0; 367 } 368 w.position = newPosition; 369 //recalculate height 370 newHeight = ratioToRaw(w.sizeRatio, ratioTotal, availableHeight); 371 if (w.size.minHeight > 0 && newHeight < w.size.minHeight){ 372 newHeight = w.size.minHeight; 373 }else if (w.size.maxHeight > 0 && newHeight > w.size.maxHeight){ 374 newHeight = w.size.maxHeight; 375 } 376 //recalculate width 377 newWidth = widgetSize.width;//default is max 378 //compare with min & max 379 if (w.size.minWidth > 0 && newWidth < w.size.minWidth){ 380 //although there isn't that much width, still assign it, that will be dealt with later 381 newWidth = w.size.minWidth; 382 }else if (w.size.maxWidth > 0 && newWidth > w.size.maxWidth){ 383 newWidth = w.size.maxWidth; 384 } 385 //check if there's not enough space available, then make it invisible 386 if (newWidth > availableWidth || newHeight > availableHeight){ 387 newWidth = 0; 388 newHeight = 0; 389 w.visible = false; 390 continue; 391 } 392 //apply new size 393 newSize = w.size;//to get min and max values 394 newSize.height = newHeight; 395 newSize.width = newWidth; 396 w.size = newSize; 397 //now the new size has been assigned, calculate amount of space & ratios left 398 availableHeight -= newHeight; 399 ratioTotal -= w.sizeRatio; 400 } 401 } 402 }else if (layoutType == LayoutDisplayType.Horizontal){ 403 //make space for new widget 404 foreach(w; widgetList){ 405 //if a widget is not visible, skip it 406 if (w.visible){ 407 //recalculate position 408 newPosition.y = widgetPosition.y;//x axis is always same, cause this is a vertical (not horizontal) layout 409 if (newWidth > 0){ 410 newPosition.x += newWidth;//add previous widget's height to get new y axis position 411 }else{ 412 newPosition.x = 0; 413 } 414 w.position = newPosition; 415 //recalculate width 416 newWidth = ratioToRaw(w.sizeRatio, ratioTotal, availableWidth); 417 if (w.size.minWidth > 0 && newWidth < w.size.minWidth){ 418 newWidth = w.size.minWidth; 419 }else if (w.size.maxWidth > 0 && newWidth > w.size.maxWidth){ 420 newWidth = w.size.maxWidth; 421 } 422 //recalculate height 423 newHeight = widgetSize.height;//default is max 424 //compare with min & max 425 if (w.size.minHeight > 0 && newHeight < w.size.minHeight){ 426 //although there isn't that much width, still assign it, that will be dealt with later 427 newHeight = w.size.minHeight; 428 }else if (w.size.maxHeight > 0 && newHeight > w.size.maxHeight){ 429 newHeight = w.size.maxHeight; 430 } 431 //check if there's not enough space available, then make it invisible 432 if (newWidth > availableWidth || newHeight > availableHeight){ 433 newWidth = 0; 434 newHeight = 0; 435 w.visible = false; 436 continue; 437 } 438 //apply new size 439 newSize = w.size;//to get min and max values 440 newSize.height = newHeight; 441 newSize.width = newWidth; 442 w.size = newSize; 443 //now the new size has been assigned, calculate amount of space & ratios left 444 availableWidth -= newWidth; 445 ratioTotal -= w.sizeRatio; 446 } 447 } 448 } 449 } 450 451 public: 452 /// Layout type 453 enum LayoutDisplayType{ 454 Vertical, 455 Horizontal, 456 } 457 this(LayoutDisplayType type){ 458 widgetName = "layout"; 459 layoutType = type; 460 activeWidget = null; 461 emptySpace.c = ' '; 462 } 463 464 override void updateColors(){ 465 needsUpdate = true; 466 if (&widgetTheme && widgetTheme.hasColors(name,["background","text"])){ 467 emptySpace.bgColor = widgetTheme.getColor(name, "background"); 468 emptySpace.textColor = widgetTheme.getColor(name, "text"); 469 }else{ 470 emptySpace.bgColor = hexToColor("000000"); 471 emptySpace.textColor = hexToColor("00FF00"); 472 } 473 if (forceUpdate !is null){ 474 forceUpdate(); 475 } 476 } 477 478 /// adds (appends) a widget to the widgetList, and makes space for it 479 void addWidget(QWidget widget){ 480 widget.theme = widgetTheme; 481 widget.updateColors(); 482 widget.onForceUpdate = forceUpdate; 483 //add it to array 484 widgetList.length++; 485 widgetList[widgetList.length-1] = widget; 486 //recalculate all widget's size to adjust 487 recalculateWidgetsSize(); 488 } 489 490 override void mouseEvent(MouseClick mouse){ 491 super.mouseEvent(mouse); 492 //check on which widget the cursor was on 493 Position p; 494 Size s; 495 uinteger i; 496 QWidget widget; 497 //remove access to cursor from previous active widget 498 if (activeWidget !is null){ 499 activeWidget.onCursorPosition = null; 500 } 501 for (i = 0; i < widgetList.length; i++){ 502 widget = widgetList[i]; 503 p = widget.position; 504 s = widget.size; 505 //check x-axis 506 if (mouse.x >= p.x && mouse.x < p.x + s.width){ 507 //check y-axis 508 if (mouse.y >= p.y && mouse.y < p.y + s.height){ 509 //give access to cursor position 510 widget.onCursorPosition = cursorPos; 511 //call mouseEvent 512 widget.mouseEvent(mouse); 513 //mark this widget as active 514 activeWidget = widget; 515 break; 516 } 517 } 518 } 519 } 520 override void keyboardEvent(KeyPress key){ 521 super.keyboardEvent(key); 522 //check active widget, call keyboardEvent 523 if (activeWidget){ 524 activeWidget.keyboardEvent(key); 525 } 526 } 527 override bool update(ref Matrix display){ 528 bool updated = false; 529 //check if already updating, case yes, return false 530 if (!isUpdating){ 531 isUpdating = true; 532 //go through all widgets, check if they need update, update them 533 Matrix wDisplay = new Matrix(1,1,emptySpace); 534 foreach(widget; widgetList){ 535 if (widget.visible){ 536 wDisplay.changeSize(widget.size.width, widget.size.height, emptySpace); 537 wDisplay.resetWritePosition(); 538 if (widget.update(wDisplay)){ 539 display.insert(wDisplay, widget.position.x, widget.position.y); 540 updated = true; 541 } 542 } 543 } 544 isUpdating = false; 545 }else{ 546 return false; 547 } 548 return updated; 549 } 550 } 551 552 /// A terminal (as the name says). 553 /// 554 /// All widgets, receives events, runs UI loop... 555 /// 556 /// Name in theme: 'terminal'; 557 class QTerminal : QLayout{ 558 private: 559 Terminal terminal; 560 RealTimeConsoleInput input; 561 Matrix termDisplay; 562 563 Position cursorPos; 564 565 bool isRunning = false; 566 public: 567 this(string caption = "QUI Text User Interface", LayoutDisplayType displayType = LayoutDisplayType.Vertical){ 568 super(displayType); 569 widgetName = "terminal"; 570 //create terminal & input 571 terminal = Terminal(ConsoleOutputType.cellular); 572 input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.allInputEvents); 573 terminal.showCursor(); 574 //init vars 575 widgetSize.height = terminal.height; 576 widgetSize.width = terminal.width; 577 widgetCaption = caption; 578 //set caption 579 terminal.setTitle(widgetCaption); 580 //create display matrix 581 termDisplay = new Matrix(widgetSize.width, widgetSize.height, emptySpace); 582 //create theme 583 widgetTheme = new QTheme; 584 } 585 ~this(){ 586 terminal.clear; 587 delete termDisplay; 588 } 589 590 override public void addWidget(QWidget widget) { 591 super.addWidget(widget); 592 widget.onForceUpdate = &updateDisplay; 593 } 594 595 override public void mouseEvent(MouseClick mouse) { 596 super.mouseEvent(mouse); 597 //check on which widget the cursor was on 598 Position p; 599 Size s; 600 uinteger i; 601 QWidget widget; 602 //remove access to cursor from previous active widget 603 if (activeWidget){ 604 activeWidget.onCursorPosition = null; 605 } 606 for (i = 0; i < widgetList.length; i++){ 607 widget = widgetList[i]; 608 p = widget.position; 609 s = widget.size; 610 //check x-axis 611 if (mouse.x >= p.x && mouse.x < p.x + s.width){ 612 //check y-axis 613 if (mouse.y >= p.y && mouse.y < p.y + s.height){ 614 //give access to cursor position 615 widget.onCursorPosition = &setCursorPos; 616 //call mouseEvent 617 widget.mouseEvent(mouse); 618 //mark this widget as active 619 activeWidget = widget; 620 break; 621 } 622 } 623 } 624 } 625 626 /// Use this instead of `update` to forcefully update the terminal 627 bool updateDisplay(){ 628 //termDisplay.clear(emptySpace); 629 bool r = update(termDisplay); 630 if (r){ 631 termDisplay.flushToTerminal(this); 632 } 633 //set cursor position 634 terminal.moveTo(cast(int)cursorPos.x, cast(int)cursorPos.y); 635 terminal.showCursor(); 636 return r; 637 } 638 639 /// starts the UI loop 640 void run(){ 641 InputEvent event; 642 isRunning = true; 643 //draw the whole thing 644 recalculateWidgetsSize; 645 updateDisplay(); 646 while (isRunning){ 647 event = input.nextEvent; 648 //check event type 649 if (event.type == event.Type.KeyboardEvent){ 650 KeyPress kPress; 651 kPress.key = event.get!(event.Type.KeyboardEvent).which; 652 this.keyboardEvent(kPress); 653 updateDisplay; 654 }else if (event.type == event.Type.MouseEvent){ 655 MouseEvent mEvent = event.get!(event.Type.MouseEvent); 656 MouseClick mPos; 657 mPos.x = mEvent.x; 658 mPos.y = mEvent.y; 659 switch (mEvent.buttons){ 660 case MouseEvent.Button.Left: 661 mPos.mouseButton = mPos.Button.Left; 662 break; 663 case MouseEvent.Button.Right: 664 mPos.mouseButton = mPos.Button.Right; 665 break; 666 case MouseEvent.Button.ScrollUp: 667 mPos.mouseButton = mPos.Button.ScrollUp; 668 break; 669 case MouseEvent.Button.ScrollDown: 670 mPos.mouseButton = mPos.Button.ScrollDown; 671 break; 672 default: 673 continue; 674 } 675 this.mouseEvent(mPos); 676 updateDisplay; 677 }else if (event.type == event.Type.SizeChangedEvent){ 678 //change matrix size 679 termDisplay.changeSize(cast(uinteger)terminal.width, cast(uinteger)terminal.height, emptySpace); 680 //update self size 681 terminal.updateSize; 682 widgetSize.height = terminal.height; 683 widgetSize.width = terminal.width; 684 //call size change on all widgets 685 recalculateWidgetsSize; 686 updateDisplay; 687 }else if (event.type == event.Type.UserInterruptionEvent){ 688 //die here 689 terminal.clear; 690 isRunning = false; 691 break; 692 } 693 } 694 //in case an exception prevents it from being set to false before 695 isRunning = false; 696 } 697 698 /// terminates the UI loop 699 void terminate(){ 700 isRunning = false; 701 } 702 703 //override write properties 704 override @property Size size(Size newSize){ 705 //don't let anything modify the size 706 return widgetSize; 707 } 708 override @property Position position(Position newPosition){ 709 return widgetPosition; 710 } 711 /// Called by active-widget(s?) to position the cursor 712 void setCursorPos(uinteger x, uinteger y){ 713 cursorPos.x = x; 714 cursorPos.y = y; 715 } 716 717 ///returns true if UI loop is running 718 @property bool running(){ 719 return isRunning; 720 } 721 722 //functions below are used by Matrix.flushToTerminal 723 ///flush changes to terminal, called by Matrix 724 void flush(){ 725 terminal.flush; 726 } 727 ///clear terminal, called before writing, called by Matrix 728 void clear(){ 729 terminal.clear; 730 } 731 ///change colors, called by Matrix 732 void setColors(RGBColor textColor, RGBColor bgColor){ 733 terminal.setTrueColor(textColor, bgColor); 734 } 735 ///move write-cursor to a position, called by Matrix 736 void moveTo(int x, int y){ 737 terminal.moveTo(x, y); 738 } 739 ///write chars to terminal, called by Matrix 740 void writeChars(char[] c){ 741 terminal.write(c); 742 } 743 ///write char to terminal, called by Matrix 744 void writeChars(char c){ 745 terminal.write(c); 746 } 747 } 748 749 ///Theme class 750 class QTheme{ 751 private: 752 RGBColor[string][string] colors; 753 RGBColor[string] globalColors;//i.e default colors 754 public: 755 this(string themeFile = null){ 756 if (themeFile != null){ 757 loadTheme(themeFile); 758 } 759 } 760 ///returns color, provided the widgetName, and which-color (like textColor). 761 /// 762 ///if not found, returns a default color provided by theme. If that color 763 ///is also not found, throws exception 764 RGBColor getColor(string widgetName, string which){ 765 if (widgetName in colors && which in colors[widgetName]){ 766 return colors[widgetName][which]; 767 }else{ 768 if (which in globalColors){ 769 return globalColors[which]; 770 }else{ 771 throw new Exception("Color "~which~" not defined (for "~widgetName~')'); 772 } 773 } 774 } 775 ///gets all colors for a widget. 776 /// 777 ///Throws exception if that widget has no colors defined in theme 778 RGBColor[string] getColors(string widgetName){ 779 if (widgetName in colors){ 780 return colors[widgetName]; 781 }else{ 782 throw new Exception("Widget "~widgetName~" not defined"); 783 } 784 } 785 /// sets a color for a widget 786 void setColor(string widgetName, string which, RGBColor color){ 787 colors[widgetName][which] = color; 788 } 789 ///sets a default value for a color 790 /// 791 ///i.e a color that is used when the color for widget is not found 792 void setColor(string which, RGBColor color){ 793 globalColors[which] = color; 794 } 795 ///sets all colors for a widget 796 void setColors(string widgetName, RGBColor[string] widgetColors){ 797 colors[widgetName] = widgetColors; 798 } 799 ///Saves current theme to a file, throws exception if failed 800 bool saveTheme(string filename){ 801 bool r = true; 802 try{ 803 File f = File(filename, "w"); 804 foreach(widgetName; colors.keys){ 805 foreach(colorName; colors[widgetName].keys){ 806 f.write(widgetName,' ',colorName,' ',colorToHex(colors[widgetName][colorName]),'\n'); 807 } 808 } 809 foreach(colorName; globalColors.keys){ 810 f.write("* ",colorName,' ',colorToHex(globalColors[colorName]),'\n'); 811 } 812 f.close; 813 }catch(Exception e){ 814 throw e; 815 } 816 return r; 817 } 818 ///Loads a theme from file, throws exception if failed 819 bool loadTheme(string filename){ 820 bool r = true; 821 try{ 822 string[] fcontents = fileToArray(filename); 823 string widgetName, colorName, colorCode, line; 824 uinteger lEnd; 825 for (uinteger lno = 0; lno < fcontents.length; lno++){ 826 line = fcontents[lno]; 827 uinteger readFrom = 0; 828 lEnd = line.length - 1; 829 for (uinteger i = 0; i < line.length; i++){ 830 if (line[i] == ' ' || i == lEnd){ 831 if (widgetName == null){ 832 widgetName = line[readFrom .. i]; 833 } 834 if (colorName == null){ 835 colorName = line[readFrom .. i]; 836 } 837 if (colorCode == null){ 838 colorCode = line[readFrom .. i]; 839 } 840 readFrom = i+1; 841 } 842 } 843 //add color, if any 844 if (widgetName && colorName && colorCode){ 845 if (widgetName == "*"){ 846 globalColors[colorName] = hexToColor(colorCode); 847 }else{ 848 colors[widgetName][colorName] = hexToColor(colorCode); 849 } 850 //clear name ... 851 widgetName, colorName, colorCode = null; 852 } 853 } 854 }catch(Exception e){ 855 throw e; 856 } 857 return r; 858 } 859 860 ///checks if theme has any color(s) for a widget 861 bool hasWidget(string widgetName){ 862 if (widgetName in colors){ 863 return true; 864 }else{ 865 return false; 866 } 867 } 868 ///checks if theme has a specific color for a specific widget 869 bool hasColor(string widgetName, string colorName){ 870 if (widgetName in colors){ 871 if (colorName in colors[widgetName]){ 872 return true; 873 }else{ 874 return false; 875 } 876 }else{ 877 return false; 878 } 879 } 880 ///checks if theme has specific colors for a specific widget 881 bool hasColors(string widgetName, string[] colorNames){ 882 bool r = true; 883 foreach(color; colorNames){ 884 if (hasColor(widgetName, color) == false){ 885 r = false; 886 break; 887 } 888 } 889 return r; 890 } 891 ///checks if theme has a default color 892 bool hasColor(string colorName){ 893 if (colorName in globalColors){ 894 return true; 895 }else{ 896 return false; 897 } 898 }///checks if theme has default colors 899 bool hasColors(string[] colorNames){ 900 bool r = true; 901 foreach(color; colorNames){ 902 if (hasColor(color) == false){ 903 r = false; 904 break; 905 } 906 } 907 return r; 908 } 909 } 910 911 //misc functions: 912 ///Center-aligns text, returns that in an char[] with width as length. The empty part filled with ' ' 913 char[] centerAlignText(char[] text, uinteger width, char fill = ' '){ 914 char[] r; 915 if (text.length < width){ 916 r.length = width; 917 uinteger offset = (width - text.length)/2; 918 r[0 .. offset] = fill; 919 r[offset .. offset+text.length][] = text; 920 r[offset+text.length .. r.length] = fill; 921 }else{ 922 r = text[0 .. width]; 923 } 924 return r; 925 } 926 927 ///used to calculate height/width using sizeRation 928 uinteger ratioToRaw(uinteger selectedRatio, uinteger ratioTotal, uinteger total){ 929 uinteger r; 930 r = cast(uinteger)((cast(float)selectedRatio/cast(float)ratioTotal)*total); 931 return r; 932 } 933 934 ///Converts hex color code to RGBColor 935 RGBColor hexToColor(string hex){ 936 RGBColor r; 937 r.r = cast(ubyte)hexToDen(hex[0..2]); 938 r.g = cast(ubyte)hexToDen(hex[2..4]); 939 r.b = cast(ubyte)hexToDen(hex[4..6]); 940 return r; 941 } 942 943 ///Converts RGBColor to hex color code 944 string colorToHex(RGBColor col){ 945 char[] r; 946 char[] code; 947 r.length = 6; 948 r[0 .. 6] = '0'; 949 code = cast(char[])denToHex(col.r); 950 r[2 - code.length .. 2] = code; 951 code = cast(char[])denToHex(col.g); 952 r[4 - code.length .. 4] = code; 953 code = cast(char[])denToHex(col.b); 954 r[6 - code.length .. 6] = code; 955 return cast(string)r; 956 }