1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // FIXME: slider widget. 4 // FIXME: number widget 5 6 // osx style menu search. 7 8 // would be cool for a scroll bar to have marking capabilities 9 // kinda like vim's marks just on clicks etc and visual representation 10 // generically. may be cool to add an up arrow to the bottom too 11 // 12 // leave a shadow of where you last were for going back easily 13 14 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 15 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 16 // the window. 17 18 // so what about context menus? 19 20 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 21 22 // FIXME: make the scroll thing go to bottom when the content changes. 23 24 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image 25 26 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 27 28 29 // FIXME: add a command search thingy built in and implement tip. 30 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 31 32 // On Windows: 33 // FIXME: various labels look broken in high contrast mode 34 // FIXME: changing themes while the program is upen doesn't trigger a redraw 35 36 // add note about manifest to documentation. also icons. 37 38 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 39 // FIXME: clear the corner of scrollbars if they pop up 40 41 // minigui needs to have a stdout redirection for gui mode on windows writeln 42 43 // I kinda wanna do state reacting. sort of. idk tho 44 45 // need a viewer widget that works like a web page - arrows scroll down consistently 46 47 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 48 49 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 50 // and help info about menu items. 51 // and search in menus? 52 53 // FIXME: a scroll area event signaling when a thing comes into view might be good 54 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 55 56 // FIXME: unify Windows style line endings 57 58 /* 59 TODO: 60 61 pie menu 62 63 class Form with submit behavior -- see AutomaticDialog 64 65 disabled widgets and menu items 66 67 TrackBar controls 68 69 event cleanup 70 tooltips. 71 api improvements 72 73 margins are kinda broken, they don't collapse like they should. at least. 74 75 a table form btw would be a horizontal layout of vertical layouts holding each column 76 that would give the same width things 77 */ 78 79 /* 80 81 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets 82 */ 83 84 /++ 85 minigui is a smallish GUI widget library, aiming to be on par with at least 86 HTML4 forms and a few other expected gui components. It uses native controls 87 on Windows and does its own thing on Linux (Mac is not currently supported but 88 may be later, and should use native controls) to keep size down. The Linux 89 appearance is similar to Windows 95 and avoids using images to maintain network 90 efficiency on remote X connections, though you can customize that. 91 92 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color]. 93 94 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 95 It isn't hugely concerned with appearance - on Windows, it just uses the native 96 controls and native theme, and on Linux, it keeps it simple and I may change that 97 at any time. 98 99 I love Qt, if you want something full featured, use it! But if you want something 100 you can just drop into a small project and expect the basics to work without outside 101 dependencies, hopefully minigui will work for you. 102 103 The event model is similar to what you use in the browser with Javascript and the 104 layout engine tries to automatically fit things in, similar to a css flexbox. 105 106 107 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 108 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 109 console and other visual bugs. 110 111 HTML_To_Classes: 112 `<input type="text">` = [LineEdit] 113 `<textarea>` = [TextEdit] 114 `<select>` = [DropDownSelection] 115 `<input type="checkbox">` = [Checkbox] 116 `<input type="radio">` = [Radiobox] 117 `<button>` = [Button] 118 119 120 Stretchiness: 121 The default is 4. You can use larger numbers for things that should 122 consume a lot of space, and lower numbers for ones that are better at 123 smaller sizes. 124 125 Overlapped_input: 126 COMING SOON: 127 minigui will include a little bit of I/O functionality that just works 128 with the event loop. If you want to get fancy, I suggest spinning up 129 another thread and posting events back and forth. 130 131 $(H2 Add ons) 132 133 $(H2 XML definitions) 134 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 135 136 $(H3 Scriptability) 137 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 138 in this documentation, it means you can call it from the script language. 139 140 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 141 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 142 143 --- 144 import arsd.minigui_xml; 145 import arsd.script; 146 147 var globals = var.emptyObject; 148 globals.makeWidgetFromString = &makeWidgetFromString; 149 150 // this now works 151 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 152 --- 153 154 More to come. 155 +/ 156 module arsd.minigui; 157 158 public import arsd.simpledisplay; 159 private alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 160 161 version(Windows) { 162 import core.sys.windows.winnls; 163 import core.sys.windows.windef; 164 import core.sys.windows.basetyps; 165 import core.sys.windows.winbase; 166 import core.sys.windows.winuser; 167 import core.sys.windows.wingdi; 168 static import gdi = core.sys.windows.wingdi; 169 } 170 171 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 172 private bool lastDefaultPrevented; 173 174 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 175 alias scriptable = arsd_jsvar_compatible; 176 177 version(Windows) { 178 // use native widgets when available unless specifically asked otherwise 179 version(custom_widgets) { 180 enum bool UsingCustomWidgets = true; 181 enum bool UsingWin32Widgets = false; 182 } else { 183 version = win32_widgets; 184 enum bool UsingCustomWidgets = false; 185 enum bool UsingWin32Widgets = true; 186 } 187 // and native theming when needed 188 //version = win32_theming; 189 } else { 190 enum bool UsingCustomWidgets = true; 191 enum bool UsingWin32Widgets = false; 192 version=custom_widgets; 193 } 194 195 196 197 /* 198 199 The main goals of minigui.d are to: 200 1) Provide basic widgets that just work in a lightweight lib. 201 I basically want things comparable to a plain HTML form, 202 plus the easy and obvious things you expect from Windows 203 apps like a menu. 204 2) Use native things when possible for best functionality with 205 least library weight. 206 3) Give building blocks to provide easy extension for your 207 custom widgets, or hooking into additional native widgets 208 I didn't wrap. 209 4) Provide interfaces for easy interaction between third 210 party minigui extensions. (event model, perhaps 211 signals/slots, drop-in ease of use bits.) 212 5) Zero non-system dependencies, including Phobos as much as 213 I reasonably can. It must only import arsd.color and 214 my simpledisplay.d. If you need more, it will have to be 215 an extension module. 216 6) An easy layout system that generally works. 217 218 A stretch goal is to make it easy to make gui forms with code, 219 some kind of resource file (xml?) and even a wysiwyg designer. 220 221 Another stretch goal is to make it easy to hook data into the gui, 222 including from reflection. So like auto-generate a form from a 223 function signature or struct definition, or show a list from an 224 array that automatically updates as the array is changed. Then, 225 your program focuses on the data more than the gui interaction. 226 227 228 229 STILL NEEDED: 230 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 231 * slider 232 * listbox 233 * spinner 234 * label? 235 * rich text 236 */ 237 238 alias HWND=void*; 239 240 /// 241 abstract class ComboboxBase : Widget { 242 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 243 // or to always show the list, we want CBS_SIMPLE == 1 244 version(win32_widgets) 245 this(uint style, Widget parent = null) { 246 super(parent); 247 createWin32Window(this, "ComboBox"w, null, style); 248 } 249 else version(custom_widgets) 250 this(Widget parent = null) { 251 super(parent); 252 253 addEventListener("keydown", (Event event) { 254 if(event.key == Key.Up) { 255 if(selection > -1) { // -1 means select blank 256 selection--; 257 fireChangeEvent(); 258 } 259 event.preventDefault(); 260 } 261 if(event.key == Key.Down) { 262 if(selection + 1 < options.length) { 263 selection++; 264 fireChangeEvent(); 265 } 266 event.preventDefault(); 267 } 268 269 }); 270 271 } 272 else static assert(false); 273 274 private string[] options; 275 private int selection = -1; 276 277 void addOption(string s) { 278 options ~= s; 279 version(win32_widgets) 280 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 281 } 282 283 void setSelection(int idx) { 284 selection = idx; 285 version(win32_widgets) 286 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 287 288 auto t = new Event(EventType.change, this); 289 t.intValue = selection; 290 t.stringValue = selection == -1 ? null : options[selection]; 291 t.dispatch(); 292 } 293 294 version(win32_widgets) 295 override void handleWmCommand(ushort cmd, ushort id) { 296 selection = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 297 fireChangeEvent(); 298 } 299 300 private void fireChangeEvent() { 301 if(selection >= options.length) 302 selection = -1; 303 auto event = new Event(EventType.change, this); 304 event.intValue = selection; 305 event.stringValue = selection == -1 ? null : options[selection]; 306 event.dispatch(); 307 } 308 309 version(win32_widgets) { 310 override int minHeight() { return Window.lineHeight + 6; } 311 override int maxHeight() { return Window.lineHeight + 6; } 312 } else { 313 override int minHeight() { return Window.lineHeight + 4; } 314 override int maxHeight() { return Window.lineHeight + 4; } 315 } 316 317 version(custom_widgets) { 318 SimpleWindow dropDown; 319 void popup() { 320 auto w = width; 321 // FIXME: suggestedDropdownHeight see below 322 auto h = cast(int) this.options.length * Window.lineHeight + 8; 323 324 auto coord = this.globalCoordinates(); 325 auto dropDown = new SimpleWindow( 326 w, h, 327 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow/*, window*/); 328 329 dropDown.move(coord.x, coord.y + this.height); 330 331 { 332 auto painter = dropDown.draw(); 333 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen); 334 auto p = Point(4, 4); 335 painter.outlineColor = Color.black; 336 foreach(option; options) { 337 painter.drawText(p, option); 338 p.y += Window.lineHeight; 339 } 340 } 341 342 dropDown.setEventHandlers( 343 (MouseEvent event) { 344 if(event.type == MouseEventType.buttonReleased) { 345 auto element = (event.y - 4) / Window.lineHeight; 346 if(element >= 0 && element <= options.length) { 347 selection = element; 348 349 fireChangeEvent(); 350 } 351 dropDown.close(); 352 } 353 } 354 ); 355 356 dropDown.show(); 357 dropDown.grabInput(); 358 } 359 360 } 361 } 362 363 /++ 364 A drop-down list where the user must select one of the 365 given options. Like `<select>` in HTML. 366 +/ 367 class DropDownSelection : ComboboxBase { 368 this(Widget parent = null) { 369 version(win32_widgets) 370 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 371 else version(custom_widgets) { 372 super(parent); 373 374 addEventListener("focus", () { this.redraw; }); 375 addEventListener("blur", () { this.redraw; }); 376 addEventListener(EventType.change, () { this.redraw; }); 377 addEventListener("mousedown", () { this.focus(); this.popup(); }); 378 addEventListener("keydown", (Event event) { 379 if(event.key == Key.Space) 380 popup(); 381 }); 382 } else static assert(false); 383 } 384 385 version(custom_widgets) 386 override void paint(WidgetPainter painter) { 387 draw3dFrame(this, painter, FrameStyle.risen); 388 painter.outlineColor = Color.black; 389 painter.drawText(Point(4, 4), selection == -1 ? "" : options[selection]); 390 391 painter.outlineColor = Color.black; 392 painter.fillColor = Color.black; 393 Point[4] triangle; 394 enum padding = 6; 395 enum paddingV = 7; 396 enum triangleWidth = 10; 397 triangle[0] = Point(width - padding - triangleWidth, paddingV); 398 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 399 triangle[2] = Point(width - padding - 0, paddingV); 400 triangle[3] = triangle[0]; 401 painter.drawPolygon(triangle[]); 402 403 if(isFocused()) { 404 painter.fillColor = Color.transparent; 405 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 406 painter.drawRectangle(Point(2, 2), width - 4, height - 4); 407 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 408 409 } 410 } 411 412 version(win32_widgets) 413 override void registerMovement() { 414 version(win32_widgets) { 415 if(hwnd) { 416 auto pos = getChildPositionRelativeToParentHwnd(this); 417 // the height given to this from Windows' perspective is supposed 418 // to include the drop down's height. so I add to it to give some 419 // room for that. 420 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 421 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 422 } 423 } 424 sendResizeEvent(); 425 } 426 427 } 428 429 /++ 430 A text box with a drop down arrow listing selections. 431 The user can choose from the list, or type their own. 432 +/ 433 class FreeEntrySelection : ComboboxBase { 434 this(Widget parent = null) { 435 version(win32_widgets) 436 super(2 /* CBS_DROPDOWN */, parent); 437 else version(custom_widgets) { 438 super(parent); 439 auto hl = new HorizontalLayout(this); 440 lineEdit = new LineEdit(hl); 441 442 tabStop = false; 443 444 lineEdit.addEventListener("focus", &lineEdit.selectAll); 445 446 auto btn = new class ArrowButton { 447 this() { 448 super(ArrowDirection.down, hl); 449 } 450 override int maxHeight() { 451 return int.max; 452 } 453 }; 454 //btn.addDirectEventListener("focus", &lineEdit.focus); 455 btn.addEventListener("triggered", &this.popup); 456 addEventListener(EventType.change, (Event event) { 457 lineEdit.content = event.stringValue; 458 lineEdit.focus(); 459 redraw(); 460 }); 461 } 462 else static assert(false); 463 } 464 465 version(custom_widgets) { 466 LineEdit lineEdit; 467 } 468 } 469 470 /++ 471 A combination of free entry with a list below it. 472 +/ 473 class ComboBox : ComboboxBase { 474 this(Widget parent = null) { 475 version(win32_widgets) 476 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 477 else version(custom_widgets) { 478 super(parent); 479 lineEdit = new LineEdit(this); 480 listWidget = new ListWidget(this); 481 listWidget.multiSelect = false; 482 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 483 string c = null; 484 foreach(option; listWidget.options) 485 if(option.selected) { 486 c = option.label; 487 break; 488 } 489 lineEdit.content = c; 490 }); 491 492 listWidget.tabStop = false; 493 this.tabStop = false; 494 listWidget.addEventListener("focus", &lineEdit.focus); 495 this.addEventListener("focus", &lineEdit.focus); 496 497 addDirectEventListener(EventType.change, { 498 listWidget.setSelection(selection); 499 if(selection != -1) 500 lineEdit.content = options[selection]; 501 lineEdit.focus(); 502 redraw(); 503 }); 504 505 lineEdit.addEventListener("focus", &lineEdit.selectAll); 506 507 listWidget.addDirectEventListener(EventType.change, { 508 int set = -1; 509 foreach(idx, opt; listWidget.options) 510 if(opt.selected) { 511 set = cast(int) idx; 512 break; 513 } 514 if(set != selection) 515 this.setSelection(set); 516 }); 517 } else static assert(false); 518 } 519 520 override int minHeight() { return Window.lineHeight * 3; } 521 override int maxHeight() { return int.max; } 522 override int heightStretchiness() { return 5; } 523 524 version(custom_widgets) { 525 LineEdit lineEdit; 526 ListWidget listWidget; 527 528 override void addOption(string s) { 529 listWidget.options ~= ListWidget.Option(s); 530 ComboboxBase.addOption(s); 531 } 532 } 533 } 534 535 /+ 536 class Spinner : Widget { 537 version(win32_widgets) 538 this(Widget parent = null) { 539 super(parent); 540 parentWindow = parent.parentWindow; 541 auto hlayout = new HorizontalLayout(this); 542 lineEdit = new LineEdit(hlayout); 543 upDownControl = new UpDownControl(hlayout); 544 } 545 546 LineEdit lineEdit; 547 UpDownControl upDownControl; 548 } 549 550 class UpDownControl : Widget { 551 version(win32_widgets) 552 this(Widget parent = null) { 553 super(parent); 554 parentWindow = parent.parentWindow; 555 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 556 } 557 558 override int minHeight() { return Window.lineHeight; } 559 override int maxHeight() { return Window.lineHeight * 3/2; } 560 561 override int minWidth() { return Window.lineHeight * 3/2; } 562 override int maxWidth() { return Window.lineHeight * 3/2; } 563 } 564 +/ 565 566 /+ 567 class DataView : Widget { 568 // this is the omnibus data viewer 569 // the internal data layout is something like: 570 // string[string][] but also each node can have parents 571 } 572 +/ 573 574 575 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 576 577 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 578 579 // FIXME: menus should prolly capture the mouse. ugh i kno. 580 /* 581 TextEdit needs: 582 583 * caret manipulation 584 * selection control 585 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 586 587 For example: 588 589 connect(paste, &textEdit.insertTextAtCaret); 590 591 would be nice. 592 593 594 595 I kinda want an omnibus dataview that combines list, tree, 596 and table - it can be switched dynamically between them. 597 598 Flattening policy: only show top level, show recursive, show grouped 599 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 600 601 Single select, multi select, organization, drag+drop 602 */ 603 604 //static if(UsingSimpledisplayX11) 605 version(win32_widgets) {} 606 else version(custom_widgets) { 607 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 608 enum activeTabColor = lightAccentColor; 609 enum hoveringColor = Color(228, 228, 228); 610 enum buttonColor = windowBackgroundColor; 611 enum depressedButtonColor = darkAccentColor; 612 enum activeListXorColor = Color(255, 255, 127); 613 enum progressBarColor = Color(0, 0, 128); 614 enum activeMenuItemColor = Color(0, 0, 128); 615 616 enum scrollClickRepeatInterval = 50; 617 } 618 else static assert(false); 619 // these are used by horizontal rule so not just custom_widgets. for now at least. 620 enum darkAccentColor = Color(172, 172, 172); 621 enum lightAccentColor = Color(223, 223, 223); // used to be 223 622 623 private const(wchar)* toWstringzInternal(in char[] s) { 624 wchar[] str; 625 str.reserve(s.length + 1); 626 foreach(dchar ch; s) 627 str ~= ch; 628 str ~= '\0'; 629 return str.ptr; 630 } 631 632 static if(SimpledisplayTimerAvailable) 633 void setClickRepeat(Widget w, int interval, int delay = 250) { 634 Timer timer; 635 int delayRemaining = delay / interval; 636 if(delayRemaining <= 1) 637 delayRemaining = 2; 638 639 immutable originalDelayRemaining = delayRemaining; 640 641 w.addDirectEventListener("mousedown", (Event ev) { 642 if(ev.srcElement !is w) 643 return; 644 if(timer !is null) { 645 timer.destroy(); 646 timer = null; 647 } 648 delayRemaining = originalDelayRemaining; 649 timer = new Timer(interval, () { 650 if(delayRemaining > 0) 651 delayRemaining--; 652 else { 653 auto ev = new Event("click", w); 654 ev.sendDirectly(); 655 } 656 }); 657 }); 658 659 w.addDirectEventListener("mouseup", (Event ev) { 660 if(ev.srcElement !is w) 661 return; 662 if(timer !is null) { 663 timer.destroy(); 664 timer = null; 665 } 666 }); 667 668 w.addDirectEventListener("mouseleave", (Event ev) { 669 if(ev.srcElement !is w) 670 return; 671 if(timer !is null) { 672 timer.destroy(); 673 timer = null; 674 } 675 }); 676 677 } 678 else 679 void setClickRepeat(Widget w, int interval, int delay = 250) {} 680 681 enum FrameStyle { 682 risen, 683 sunk 684 } 685 686 version(custom_widgets) 687 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background = windowBackgroundColor) { 688 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 689 } 690 691 version(custom_widgets) 692 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background = windowBackgroundColor) { 693 // outer layer 694 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 695 painter.fillColor = background; 696 painter.drawRectangle(Point(x + 0, y + 0), width, height); 697 698 painter.outlineColor = (style == FrameStyle.sunk) ? darkAccentColor : lightAccentColor; 699 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 700 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 701 702 // inner layer 703 //right, bottom 704 painter.outlineColor = (style == FrameStyle.sunk) ? lightAccentColor : darkAccentColor; 705 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 706 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 707 // left, top 708 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 709 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 710 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 711 } 712 713 /// 714 class Action { 715 version(win32_widgets) { 716 private int id; 717 private static int lastId = 9000; 718 private static Action[int] mapping; 719 } 720 721 KeyEvent accelerator; 722 723 /// 724 this(string label, ushort icon = 0, void delegate() triggered = null) { 725 this.label = label; 726 this.iconId = icon; 727 if(triggered !is null) 728 this.triggered ~= triggered; 729 version(win32_widgets) { 730 id = ++lastId; 731 mapping[id] = this; 732 } 733 } 734 735 private string label; 736 private ushort iconId; 737 // icon 738 739 // when it is triggered, the triggered event is fired on the window 740 void delegate()[] triggered; 741 } 742 743 /* 744 plan: 745 keyboard accelerators 746 747 * menus (and popups and tooltips) 748 * status bar 749 * toolbars and buttons 750 751 sortable table view 752 753 maybe notification area icons 754 basic clipboard 755 756 * radio box 757 splitter 758 toggle buttons (optionally mutually exclusive, like in Paint) 759 label, rich text display, multi line plain text (selectable) 760 * fieldset 761 * nestable grid layout 762 single line text input 763 * multi line text input 764 slider 765 spinner 766 list box 767 drop down 768 combo box 769 auto complete box 770 * progress bar 771 772 terminal window/widget (on unix it might even be a pty but really idk) 773 774 ok button 775 cancel button 776 777 keyboard hotkeys 778 779 scroll widget 780 781 event redirections and network transparency 782 script integration 783 */ 784 785 786 /* 787 MENUS 788 789 auto bar = new MenuBar(window); 790 window.menuBar = bar; 791 792 auto fileMenu = bar.addItem(new Menu("&File")); 793 fileMenu.addItem(new MenuItem("&Exit")); 794 795 796 EVENTS 797 798 For controls, you should usually use "triggered" rather than "click", etc., because 799 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 800 This is the case on menus and pushbuttons. 801 802 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 803 */ 804 805 806 /* 807 enum LinePreference { 808 AlwaysOnOwnLine, // always on its own line 809 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 810 PreferToShareLine, // does not force new line, and if the next child likes to share too, they will div it up evenly. otherwise, it will expand as much as it can 811 } 812 */ 813 814 mixin template Padding(string code) { 815 override int paddingLeft() { return mixin(code);} 816 override int paddingRight() { return mixin(code);} 817 override int paddingTop() { return mixin(code);} 818 override int paddingBottom() { return mixin(code);} 819 } 820 821 mixin template Margin(string code) { 822 override int marginLeft() { return mixin(code);} 823 override int marginRight() { return mixin(code);} 824 override int marginTop() { return mixin(code);} 825 override int marginBottom() { return mixin(code);} 826 } 827 828 829 mixin template LayoutInfo() { 830 int minWidth() { return 0; } 831 int minHeight() { 832 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 833 int sum = 0; 834 foreach(child; children) { 835 sum += child.minHeight(); 836 sum += child.marginTop(); 837 sum += child.marginBottom(); 838 } 839 840 return sum; 841 } 842 int maxWidth() { return int.max; } 843 int maxHeight() { return int.max; } 844 int widthStretchiness() { return 4; } 845 int heightStretchiness() { return 4; } 846 847 int marginLeft() { return 0; } 848 int marginRight() { return 0; } 849 int marginTop() { return 0; } 850 int marginBottom() { return 0; } 851 int paddingLeft() { return 0; } 852 int paddingRight() { return 0; } 853 int paddingTop() { return 0; } 854 int paddingBottom() { return 0; } 855 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 856 857 void recomputeChildLayout() { 858 .recomputeChildLayout!"height"(this); 859 } 860 } 861 862 private 863 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 864 enum calcingV = relevantMeasure == "height"; 865 866 parent.registerMovement(); 867 868 if(parent.children.length == 0) 869 return; 870 871 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 872 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 873 874 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 875 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 876 877 // my own width and height should already be set by the caller of this function... 878 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 879 mixin("parent.padding"~firstThingy~"()") - 880 mixin("parent.padding"~secondThingy~"()"); 881 882 int stretchinessSum; 883 int stretchyChildSum; 884 int lastMargin = 0; 885 886 // set initial size 887 foreach(child; parent.children) { 888 if(cast(StaticPosition) child) 889 continue; 890 if(child.hidden) 891 continue; 892 893 static if(calcingV) { 894 child.width = parent.width - 895 mixin("child.margin"~otherFirstThingy~"()") - 896 mixin("child.margin"~otherSecondThingy~"()") - 897 mixin("parent.padding"~otherFirstThingy~"()") - 898 mixin("parent.padding"~otherSecondThingy~"()"); 899 900 if(child.width < 0) 901 child.width = 0; 902 if(child.width > child.maxWidth()) 903 child.width = child.maxWidth(); 904 child.height = child.minHeight(); 905 } else { 906 child.height = parent.height - 907 mixin("child.margin"~firstThingy~"()") - 908 mixin("child.margin"~secondThingy~"()") - 909 mixin("parent.padding"~firstThingy~"()") - 910 mixin("parent.padding"~secondThingy~"()"); 911 if(child.height < 0) 912 child.height = 0; 913 if(child.height > child.maxHeight()) 914 child.height = child.maxHeight(); 915 child.width = child.minWidth(); 916 } 917 918 spaceRemaining -= mixin("child." ~ relevantMeasure); 919 920 int thisMargin = mymax(lastMargin, mixin("child.margin"~firstThingy~"()")); 921 auto margin = mixin("child.margin" ~ secondThingy ~ "()"); 922 lastMargin = margin; 923 spaceRemaining -= thisMargin + margin; 924 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 925 stretchinessSum += s; 926 if(s > 0) 927 stretchyChildSum++; 928 } 929 930 // stretch to fill space 931 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 932 //import std.stdio; writeln("str ", stretchinessSum); 933 auto spacePerChild = spaceRemaining / stretchinessSum; 934 bool spreadEvenly; 935 bool giveToBiggest; 936 if(spacePerChild <= 0) { 937 spacePerChild = spaceRemaining / stretchyChildSum; 938 spreadEvenly = true; 939 } 940 if(spacePerChild <= 0) { 941 giveToBiggest = true; 942 } 943 int previousSpaceRemaining = spaceRemaining; 944 stretchinessSum = 0; 945 Widget mostStretchy; 946 int mostStretchyS; 947 foreach(child; parent.children) { 948 if(cast(StaticPosition) child) 949 continue; 950 if(child.hidden) 951 continue; 952 static if(calcingV) 953 auto maximum = child.maxHeight(); 954 else 955 auto maximum = child.maxWidth(); 956 957 if(mixin("child." ~ relevantMeasure) >= maximum) { 958 auto adj = mixin("child." ~ relevantMeasure) - maximum; 959 mixin("child._" ~ relevantMeasure) -= adj; 960 spaceRemaining += adj; 961 continue; 962 } 963 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 964 if(s <= 0) 965 continue; 966 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 967 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 968 spaceRemaining -= spaceAdjustment; 969 if(mixin("child." ~ relevantMeasure) > maximum) { 970 auto diff = mixin("child." ~ relevantMeasure) - maximum; 971 mixin("child._" ~ relevantMeasure) -= diff; 972 spaceRemaining += diff; 973 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 974 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 975 if(mostStretchy is null || s >= mostStretchyS) { 976 mostStretchy = child; 977 mostStretchyS = s; 978 } 979 } 980 } 981 982 if(giveToBiggest && mostStretchy !is null) { 983 auto child = mostStretchy; 984 int spaceAdjustment = spaceRemaining; 985 986 static if(calcingV) 987 auto maximum = child.maxHeight(); 988 else 989 auto maximum = child.maxWidth(); 990 991 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 992 spaceRemaining -= spaceAdjustment; 993 if(mixin("child._" ~ relevantMeasure) > maximum) { 994 auto diff = mixin("child." ~ relevantMeasure) - maximum; 995 mixin("child._" ~ relevantMeasure) -= diff; 996 spaceRemaining += diff; 997 } 998 } 999 1000 if(spaceRemaining == previousSpaceRemaining) 1001 break; // apparently nothing more we can do 1002 } 1003 1004 // position 1005 lastMargin = 0; 1006 int currentPos = mixin("parent.padding"~firstThingy~"()"); 1007 foreach(child; parent.children) { 1008 if(cast(StaticPosition) child) { 1009 child.recomputeChildLayout(); 1010 continue; 1011 } 1012 if(child.hidden) 1013 continue; 1014 auto margin = mixin("child.margin" ~ secondThingy ~ "()"); 1015 int thisMargin = mymax(lastMargin, mixin("child.margin"~firstThingy~"()")); 1016 currentPos += thisMargin; 1017 static if(calcingV) { 1018 child.x = parent.paddingLeft() + child.marginLeft(); 1019 child.y = currentPos; 1020 } else { 1021 child.x = currentPos; 1022 child.y = parent.paddingTop() + child.marginTop(); 1023 1024 } 1025 currentPos += mixin("child." ~ relevantMeasure); 1026 currentPos += margin; 1027 lastMargin = margin; 1028 1029 child.recomputeChildLayout(); 1030 } 1031 } 1032 1033 int mymax(int a, int b) { return a > b ? a : b; } 1034 1035 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 1036 // and here, it must be integrable with the layout, the event system, and not be painted over. 1037 version(win32_widgets) { 1038 extern(Windows) 1039 private 1040 int HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 1041 //import std.stdio; try { writeln(iMessage); } catch(Exception e) {}; 1042 if(auto te = hWnd in Widget.nativeMapping) { 1043 try { 1044 1045 te.hookedWndProc(iMessage, wParam, lParam); 1046 1047 if(iMessage == WM_SETFOCUS) { 1048 auto lol = *te; 1049 while(lol !is null && lol.implicitlyCreated) 1050 lol = lol.parent; 1051 lol.focus(); 1052 //(*te).parentWindow.focusedWidget = lol; 1053 } 1054 1055 1056 1057 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 1058 SetBkMode(cast(HDC) wParam, TRANSPARENT); 1059 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 1060 //GetStockObject(NULL_BRUSH); 1061 } 1062 1063 1064 auto pos = getChildPositionRelativeToParentOrigin(*te); 1065 lastDefaultPrevented = false; 1066 // try {import std.stdio; writeln(typeid(*te)); } catch(Exception e) {} 1067 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 1068 return cast(int) CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 1069 else { 1070 // it was something we recognized, should only call the window procedure if the default was not prevented 1071 } 1072 } catch(Exception e) { 1073 assert(0, e.toString()); 1074 } 1075 return 0; 1076 } 1077 assert(0, "shouldn't be receiving messages for this window...."); 1078 //import std.conv; 1079 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 1080 } 1081 1082 extern(Windows) 1083 private 1084 int HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 1085 if(iMessage == WM_ERASEBKGND) { 1086 auto dc = GetDC(hWnd); 1087 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 1088 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 1089 RECT r; 1090 GetWindowRect(hWnd, &r); 1091 // since the pen is null, to fill the whole space, we need the +1 on both. 1092 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 1093 SelectObject(dc, p); 1094 SelectObject(dc, b); 1095 ReleaseDC(hWnd, dc); 1096 return 1; 1097 } 1098 return HookedWndProc(hWnd, iMessage, wParam, lParam); 1099 } 1100 1101 // className MUST be a string literal 1102 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 1103 assert(p.parentWindow !is null); 1104 assert(p.parentWindow.win.impl.hwnd !is null); 1105 1106 auto bsgroupbox = style == BS_GROUPBOX; 1107 1108 HWND phwnd; 1109 1110 auto wtf = p.parent; 1111 while(wtf) { 1112 if(wtf.hwnd !is null) { 1113 phwnd = wtf.hwnd; 1114 break; 1115 } 1116 wtf = wtf.parent; 1117 } 1118 1119 if(phwnd is null) 1120 phwnd = p.parentWindow.win.impl.hwnd; 1121 1122 assert(phwnd !is null); 1123 1124 WCharzBuffer wt = WCharzBuffer(windowText); 1125 1126 style |= WS_VISIBLE | WS_CHILD; 1127 //if(className != WC_TABCONTROL) 1128 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 1129 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 1130 CW_USEDEFAULT, CW_USEDEFAULT, 100, 100, 1131 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 1132 1133 assert(p.hwnd !is null); 1134 1135 1136 static HFONT font; 1137 if(font is null) { 1138 NONCLIENTMETRICS params; 1139 params.cbSize = params.sizeof; 1140 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 1141 font = CreateFontIndirect(¶ms.lfMessageFont); 1142 } 1143 } 1144 1145 if(font) 1146 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 1147 1148 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 1149 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 1150 Widget.nativeMapping[p.hwnd] = p; 1151 1152 if(bsgroupbox) 1153 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 1154 else 1155 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 1156 1157 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 1158 1159 p.registerMovement(); 1160 } 1161 } 1162 1163 version(win32_widgets) 1164 private 1165 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 1166 if(hwnd is null || hwnd in Widget.nativeMapping) 1167 return true; 1168 auto parent = cast(Widget) cast(void*) lparam; 1169 Widget p = new Widget(); 1170 p.parent = parent; 1171 p.parentWindow = parent.parentWindow; 1172 p.hwnd = hwnd; 1173 p.implicitlyCreated = true; 1174 Widget.nativeMapping[p.hwnd] = p; 1175 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 1176 return true; 1177 } 1178 1179 /++ 1180 1181 +/ 1182 struct WidgetPainter { 1183 /// 1184 ScreenPainter screenPainter; 1185 /// Forward to the screen painter for other methods 1186 alias screenPainter this; 1187 1188 /++ 1189 This is the list of rectangles that actually need to be redrawn. 1190 1191 Not actually implemented yet. 1192 +/ 1193 Rectangle[] invalidatedRectangles; 1194 1195 1196 // all this stuff is a dangerous experiment.... 1197 static class ScriptableVersion { 1198 ScreenPainterImplementation* p; 1199 int originX, originY; 1200 1201 @scriptable: 1202 void drawRectangle(int x, int y, int width, int height) { 1203 p.drawRectangle(x + originX, y + originY, width, height); 1204 } 1205 void drawLine(int x1, int y1, int x2, int y2) { 1206 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 1207 } 1208 void drawText(int x, int y, string text) { 1209 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 1210 } 1211 void setOutlineColor(int r, int g, int b) { 1212 p.pen = Pen(Color(r,g,b), 1); 1213 } 1214 void setFillColor(int r, int g, int b) { 1215 p.fillColor = Color(r,g,b); 1216 } 1217 } 1218 1219 ScriptableVersion toArsdJsvar() { 1220 auto sv = new ScriptableVersion; 1221 sv.p = this.screenPainter.impl; 1222 sv.originX = this.screenPainter.originX; 1223 sv.originY = this.screenPainter.originY; 1224 return sv; 1225 } 1226 1227 static WidgetPainter fromJsVar(T)(T t) { 1228 return WidgetPainter.init; 1229 } 1230 // done.......... 1231 } 1232 1233 /** 1234 The way this module works is it builds on top of a SimpleWindow 1235 from simpledisplay to provide simple controls and such. 1236 1237 Non-native controls suck, but nevertheless, I'm going to do it that 1238 way to avoid dependencies on stuff like gtk on X... and since I'll 1239 be writing the widgets there, I might as well just use them on Windows 1240 too if you like, using `-version=custom_widgets`. 1241 1242 So, by extension, this sucks. But gtkd is just too big for me. 1243 1244 1245 The goal is to look kinda like Windows 95, perhaps with customizability. 1246 Nothing too fancy, just the basics that work. 1247 */ 1248 class Widget { 1249 mixin LayoutInfo!(); 1250 1251 protected void sendResizeEvent() { 1252 auto event = new Event("resize", this); 1253 event.sendDirectly(); 1254 } 1255 1256 Menu contextMenu(int x, int y) { return null; } 1257 1258 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1259 if(parentWindow is null || parentWindow.win is null) return false; 1260 1261 auto menu = this.contextMenu(x, y); 1262 if(menu is null) 1263 return false; 1264 1265 version(win32_widgets) { 1266 // FIXME: if it is -1, -1, do it at the current selection location instead 1267 // tho the corner of the window, whcih it does now, isn't the literal worst. 1268 1269 if(screenX < 0 && screenY < 0) { 1270 auto p = this.globalCoordinates(); 1271 if(screenX == -2) 1272 p.x += x; 1273 if(screenY == -2) 1274 p.y += y; 1275 1276 screenX = p.x; 1277 screenY = p.y; 1278 } 1279 1280 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1281 throw new Exception("TrackContextMenuEx"); 1282 } else version(custom_widgets) { 1283 menu.popup(this, x, y); 1284 } 1285 1286 return true; 1287 } 1288 1289 /// 1290 @scriptable 1291 void removeWidget() { 1292 auto p = this.parent; 1293 if(p) { 1294 int item; 1295 for(item = 0; item < p.children.length; item++) 1296 if(p.children[item] is this) 1297 break; 1298 for(; item < p.children.length - 1; item++) 1299 p.children[item] = p.children[item + 1]; 1300 p.children = p.children[0 .. $-1]; 1301 } 1302 } 1303 1304 @scriptable 1305 Widget getChildByName(string name) { 1306 return getByName(name); 1307 } 1308 /// 1309 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1310 if(this.name == name) 1311 if(auto c = cast(WidgetClass) this) 1312 return c; 1313 foreach(child; children) { 1314 auto w = child.getByName(name); 1315 if(auto c = cast(WidgetClass) w) 1316 return c; 1317 } 1318 return null; 1319 } 1320 1321 @scriptable 1322 string name; /// 1323 1324 private EventHandler[][string] bubblingEventHandlers; 1325 private EventHandler[][string] capturingEventHandlers; 1326 1327 /++ 1328 Default event handlers. These are called on the appropriate 1329 event unless [Event.preventDefault] is called on the event at 1330 some point through the bubbling process. 1331 1332 1333 If you are implementing your own widget and want to add custom 1334 events, you should follow the same pattern here: create a virtual 1335 function named `defaultEventHandler_eventname` with the implementation, 1336 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1337 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1338 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1339 This ensures virtual dispatch based on the correct subclass. 1340 1341 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1342 overridden version. 1343 1344 You only need to do that on parent classes adding NEW event types. If you 1345 just want to change the default behavior of an existing event type in a subclass, 1346 you override the function (and optionally call `super.method_name`) like normal. 1347 1348 +/ 1349 protected EventHandler[string] defaultEventHandlers; 1350 1351 /// ditto 1352 void setupDefaultEventHandlers() { 1353 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(event); }; 1354 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(event); }; 1355 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(event); }; 1356 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(event); }; 1357 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(event); }; 1358 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(event); }; 1359 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(event); }; 1360 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(event); }; 1361 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(event); }; 1362 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(event); }; 1363 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(event); }; 1364 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1365 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1366 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1367 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1368 } 1369 1370 /// ditto 1371 void defaultEventHandler_click(Event event) {} 1372 /// ditto 1373 void defaultEventHandler_keydown(Event event) {} 1374 /// ditto 1375 void defaultEventHandler_keyup(Event event) {} 1376 /// ditto 1377 void defaultEventHandler_mousedown(Event event) {} 1378 /// ditto 1379 void defaultEventHandler_mouseover(Event event) {} 1380 /// ditto 1381 void defaultEventHandler_mouseout(Event event) {} 1382 /// ditto 1383 void defaultEventHandler_mouseup(Event event) {} 1384 /// ditto 1385 void defaultEventHandler_mousemove(Event event) {} 1386 /// ditto 1387 void defaultEventHandler_mouseenter(Event event) {} 1388 /// ditto 1389 void defaultEventHandler_mouseleave(Event event) {} 1390 /// ditto 1391 void defaultEventHandler_char(Event event) {} 1392 /// ditto 1393 void defaultEventHandler_triggered(Event event) {} 1394 /// ditto 1395 void defaultEventHandler_change(Event event) {} 1396 /// ditto 1397 void defaultEventHandler_focus(Event event) {} 1398 /// ditto 1399 void defaultEventHandler_blur(Event event) {} 1400 1401 /++ 1402 Events use a Javascript-esque scheme. 1403 1404 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1405 +/ 1406 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1407 return addEventListener(event, (Widget, Event e) { 1408 if(e.srcElement is this) 1409 handler(); 1410 }, useCapture); 1411 } 1412 1413 /// 1414 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1415 return addEventListener(event, (Widget, Event e) { 1416 if(e.srcElement is this) 1417 handler(e); 1418 }, useCapture); 1419 } 1420 1421 1422 /// 1423 @scriptable 1424 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1425 return addEventListener(event, (Widget, Event) { handler(); }, useCapture); 1426 } 1427 1428 /// 1429 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1430 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1431 } 1432 1433 /// 1434 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1435 if(event.length > 2 && event[0..2] == "on") 1436 event = event[2 .. $]; 1437 1438 if(useCapture) 1439 capturingEventHandlers[event] ~= handler; 1440 else 1441 bubblingEventHandlers[event] ~= handler; 1442 1443 return EventListener(this, event, handler, useCapture); 1444 } 1445 1446 /// 1447 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1448 if(event.length > 2 && event[0..2] == "on") 1449 event = event[2 .. $]; 1450 1451 if(useCapture) { 1452 if(event in capturingEventHandlers) 1453 foreach(ref evt; capturingEventHandlers[event]) 1454 if(evt is handler) evt = null; 1455 } else { 1456 if(event in bubblingEventHandlers) 1457 foreach(ref evt; bubblingEventHandlers[event]) 1458 if(evt is handler) evt = null; 1459 } 1460 } 1461 1462 /// 1463 void removeEventListener(EventListener listener) { 1464 removeEventListener(listener.event, listener.handler, listener.useCapture); 1465 } 1466 1467 MouseCursor cursor() { 1468 return GenericCursor.Default; 1469 } 1470 1471 static if(UsingSimpledisplayX11) { 1472 void discardXConnectionState() { 1473 foreach(child; children) 1474 child.discardXConnectionState(); 1475 } 1476 1477 void recreateXConnectionState() { 1478 foreach(child; children) 1479 child.recreateXConnectionState(); 1480 redraw(); 1481 } 1482 } 1483 1484 /// 1485 Point globalCoordinates() { 1486 int x = this.x; 1487 int y = this.y; 1488 auto p = this.parent; 1489 while(p) { 1490 x += p.x; 1491 y += p.y; 1492 p = p.parent; 1493 } 1494 1495 static if(UsingSimpledisplayX11) { 1496 auto dpy = XDisplayConnection.get; 1497 arsd.simpledisplay.Window dummyw; 1498 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1499 } else { 1500 POINT pt; 1501 pt.x = x; 1502 pt.y = y; 1503 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1504 x = pt.x; 1505 y = pt.y; 1506 } 1507 1508 return Point(x, y); 1509 } 1510 1511 version(win32_widgets) 1512 void handleWmCommand(ushort cmd, ushort id) {} 1513 1514 version(win32_widgets) 1515 int handleWmNotify(NMHDR* hdr, int code) { return 0; } 1516 1517 @scriptable 1518 string statusTip; 1519 // string toolTip; 1520 // string helpText; 1521 1522 bool tabStop = true; 1523 int tabOrder; 1524 1525 version(win32_widgets) { 1526 static Widget[HWND] nativeMapping; 1527 HWND hwnd; 1528 WNDPROC originalWindowProcedure; 1529 1530 SimpleWindow simpleWindowWrappingHwnd; 1531 1532 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1533 switch(iMessage) { 1534 case WM_COMMAND: 1535 switch(HIWORD(wParam)) { 1536 case 0: 1537 // case BN_CLICKED: aka 0 1538 case 1: 1539 auto idm = LOWORD(wParam); 1540 if(auto item = idm in Action.mapping) { 1541 foreach(handler; (*item).triggered) 1542 handler(); 1543 /* 1544 auto event = new Event("triggered", *item); 1545 event.button = idm; 1546 event.dispatch(); 1547 */ 1548 } else { 1549 auto handle = cast(HWND) lParam; 1550 if(auto widgetp = handle in Widget.nativeMapping) { 1551 (*widgetp).handleWmCommand(HIWORD(wParam), LOWORD(wParam)); 1552 } 1553 } 1554 break; 1555 default: 1556 return 0; 1557 } 1558 break; 1559 default: 1560 } 1561 1562 return 0; 1563 } 1564 } 1565 bool implicitlyCreated; 1566 1567 int x; // relative to the parent's origin 1568 int y; // relative to the parent's origin 1569 int _width; 1570 int _height; 1571 Widget[] children; 1572 Widget parent; 1573 1574 public @property int width() { return _width; } 1575 public @property int height() { return _height; } 1576 1577 protected @property int width(int a) { return _width = a; } 1578 protected @property int height(int a) { return _height = a; } 1579 1580 protected 1581 void registerMovement() { 1582 version(win32_widgets) { 1583 if(hwnd) { 1584 auto pos = getChildPositionRelativeToParentHwnd(this); 1585 MoveWindow(hwnd, pos[0], pos[1], width, height, true); 1586 } 1587 } 1588 sendResizeEvent(); 1589 } 1590 1591 Window parentWindow; 1592 1593 /// 1594 this(Widget parent = null) { 1595 if(parent !is null) 1596 parent.addChild(this); 1597 setupDefaultEventHandlers(); 1598 } 1599 1600 /// 1601 @scriptable 1602 bool isFocused() { 1603 return parentWindow && parentWindow.focusedWidget is this; 1604 } 1605 1606 private bool showing_ = true; 1607 bool showing() { return showing_; } 1608 bool hidden() { return !showing_; } 1609 void showing(bool s, bool recalculate = true) { 1610 auto so = showing_; 1611 showing_ = s; 1612 if(s != so) { 1613 1614 version(win32_widgets) 1615 if(hwnd) 1616 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1617 1618 if(parent && recalculate) { 1619 parent.recomputeChildLayout(); 1620 parent.redraw(); 1621 } 1622 1623 foreach(child; children) 1624 child.showing(s, false); 1625 } 1626 } 1627 /// 1628 @scriptable 1629 void show() { 1630 showing = true; 1631 } 1632 /// 1633 @scriptable 1634 void hide() { 1635 showing = false; 1636 } 1637 1638 /// 1639 @scriptable 1640 void focus() { 1641 assert(parentWindow !is null); 1642 if(isFocused()) 1643 return; 1644 1645 if(parentWindow.focusedWidget) { 1646 // FIXME: more details here? like from and to 1647 auto evt = new Event("blur", parentWindow.focusedWidget); 1648 parentWindow.focusedWidget = null; 1649 evt.sendDirectly(); 1650 } 1651 1652 1653 version(win32_widgets) { 1654 if(this.hwnd !is null) 1655 SetFocus(this.hwnd); 1656 } 1657 1658 parentWindow.focusedWidget = this; 1659 auto evt = new Event("focus", this); 1660 evt.dispatch(); 1661 } 1662 1663 1664 void attachedToWindow(Window w) {} 1665 void addedTo(Widget w) {} 1666 1667 private void newWindow(Window parent) { 1668 parentWindow = parent; 1669 foreach(child; children) 1670 child.newWindow(parent); 1671 } 1672 1673 protected void addChild(Widget w, int position = int.max) { 1674 w.parent = this; 1675 if(position == int.max || position == children.length) 1676 children ~= w; 1677 else { 1678 assert(position < children.length); 1679 children.length = children.length + 1; 1680 for(int i = cast(int) children.length - 1; i > position; i--) 1681 children[i] = children[i - 1]; 1682 children[position] = w; 1683 } 1684 1685 w.newWindow(this.parentWindow); 1686 1687 w.addedTo(this); 1688 1689 if(this.hidden) 1690 w.showing = false; 1691 1692 if(parentWindow !is null) { 1693 w.attachedToWindow(parentWindow); 1694 parentWindow.recomputeChildLayout(); 1695 parentWindow.redraw(); 1696 } 1697 } 1698 1699 Widget getChildAtPosition(int x, int y) { 1700 // it goes backward so the last one to show gets picked first 1701 // might use z-index later 1702 foreach_reverse(child; children) { 1703 if(child.hidden) 1704 continue; 1705 if(child.x <= x && child.y <= y 1706 && ((x - child.x) < child.width) 1707 && ((y - child.y) < child.height)) 1708 { 1709 return child; 1710 } 1711 } 1712 1713 return null; 1714 } 1715 1716 /// 1717 void paint(WidgetPainter painter) {} 1718 1719 deprecated("Change ScreenPainter to WidgetPainter") 1720 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1721 1722 /// I don't actually like the name of this 1723 /// this draws a background on it 1724 void erase(WidgetPainter painter) { 1725 version(win32_widgets) 1726 if(hwnd) return; // Windows will do it. I think. 1727 1728 auto c = backgroundColor; 1729 painter.fillColor = c; 1730 painter.outlineColor = c; 1731 1732 version(win32_widgets) { 1733 HANDLE b, p; 1734 if(c.a == 0) { 1735 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1736 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1737 } 1738 } 1739 painter.drawRectangle(Point(0, 0), width, height); 1740 version(win32_widgets) { 1741 if(c.a == 0) { 1742 SelectObject(painter.impl.hdc, p); 1743 SelectObject(painter.impl.hdc, b); 1744 } 1745 } 1746 } 1747 1748 /// 1749 Color backgroundColor() { 1750 // the default is a "transparent" background, which means 1751 // it goes as far up as it can to get the color 1752 if (backgroundColor_ != Color.transparent) 1753 return backgroundColor_; 1754 if (parent) 1755 return parent.backgroundColor(); 1756 return backgroundColor_; 1757 } 1758 1759 private Color backgroundColor_ = Color.transparent; 1760 1761 /// 1762 void backgroundColor(Color c){ 1763 this.backgroundColor_ = c; 1764 } 1765 1766 /// 1767 WidgetPainter draw() { 1768 int x = this.x, y = this.y; 1769 auto parent = this.parent; 1770 while(parent) { 1771 x += parent.x; 1772 y += parent.y; 1773 parent = parent.parent; 1774 } 1775 1776 auto painter = parentWindow.win.draw(); 1777 painter.originX = x; 1778 painter.originY = y; 1779 painter.setClipRectangle(Point(0, 0), width, height); 1780 return WidgetPainter(painter); 1781 } 1782 1783 protected void privatePaint(WidgetPainter painter, int lox, int loy, bool force = false) { 1784 if(hidden) 1785 return; 1786 1787 painter.originX = lox + x; 1788 painter.originY = loy + y; 1789 1790 bool actuallyPainted = false; 1791 1792 if(redrawRequested || force) { 1793 painter.setClipRectangle(Point(0, 0), width, height); 1794 1795 erase(painter); 1796 paint(painter); 1797 1798 redrawRequested = false; 1799 actuallyPainted = true; 1800 } 1801 1802 foreach(child; children) { 1803 version(win32_widgets) 1804 if(child.useNativeDrawing()) continue; 1805 child.privatePaint(painter, painter.originX, painter.originY, actuallyPainted); 1806 } 1807 1808 version(win32_widgets) 1809 foreach(child; children) { 1810 if(child.useNativeDrawing) { 1811 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw); 1812 child.privatePaint(painter, painter.originX, painter.originY, actuallyPainted); 1813 } 1814 } 1815 } 1816 1817 bool useNativeDrawing() nothrow { 1818 version(win32_widgets) 1819 return hwnd !is null; 1820 else 1821 return false; 1822 } 1823 1824 static class RedrawEvent {} 1825 __gshared re = new RedrawEvent(); 1826 1827 private bool redrawRequested; 1828 /// 1829 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1830 redrawRequested = true; 1831 1832 if(this.parentWindow) { 1833 auto sw = this.parentWindow.win; 1834 assert(sw !is null); 1835 if(!sw.eventQueued!RedrawEvent) { 1836 sw.postEvent(re); 1837 //import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1838 } 1839 } 1840 } 1841 1842 void actualRedraw() { 1843 if(!showing) return; 1844 1845 assert(parentWindow !is null); 1846 1847 auto w = drawableWindow; 1848 if(w is null) 1849 w = parentWindow.win; 1850 1851 if(w.closed()) 1852 return; 1853 1854 auto ugh = this.parent; 1855 int lox, loy; 1856 while(ugh) { 1857 lox += ugh.x; 1858 loy += ugh.y; 1859 ugh = ugh.parent; 1860 } 1861 auto painter = w.draw(); 1862 privatePaint(WidgetPainter(painter), lox, loy); 1863 } 1864 1865 SimpleWindow drawableWindow; 1866 } 1867 1868 /++ 1869 Nests an opengl capable window inside this window as a widget. 1870 1871 You may also just want to create an additional [SimpleWindow] with 1872 [OpenGlOptions.yes] yourself. 1873 1874 An OpenGL widget cannot have child widgets. It will throw if you try. 1875 +/ 1876 static if(OpenGlEnabled) 1877 class OpenGlWidget : Widget { 1878 SimpleWindow win; 1879 1880 /// 1881 this(Widget parent) { 1882 this.parentWindow = parent.parentWindow; 1883 1884 SimpleWindow pwin = this.parentWindow.win; 1885 1886 1887 version(win32_widgets) { 1888 HWND phwnd; 1889 auto wtf = parent; 1890 while(wtf) { 1891 if(wtf.hwnd) { 1892 phwnd = wtf.hwnd; 1893 break; 1894 } 1895 wtf = wtf.parent; 1896 } 1897 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 1898 if(phwnd) 1899 pwin = new SimpleWindow(phwnd); 1900 } 1901 1902 win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin); 1903 super(parent); 1904 1905 /* 1906 win.onFocusChange = (bool getting) { 1907 if(getting) 1908 this.focus(); 1909 }; 1910 */ 1911 1912 version(win32_widgets) { 1913 Widget.nativeMapping[win.hwnd] = this; 1914 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 1915 } else { 1916 win.setEventHandlers( 1917 (MouseEvent e) { 1918 Widget p = this; 1919 while(p ! is parentWindow) { 1920 e.x += p.x; 1921 e.y += p.y; 1922 p = p.parent; 1923 } 1924 parentWindow.dispatchMouseEvent(e); 1925 }, 1926 (KeyEvent e) { 1927 //import std.stdio; 1928 //writefln("%x %s", cast(uint) e.key, e.key); 1929 parentWindow.dispatchKeyEvent(e); 1930 }, 1931 (dchar e) { 1932 parentWindow.dispatchCharEvent(e); 1933 }, 1934 ); 1935 } 1936 } 1937 1938 override void paint(WidgetPainter painter) { 1939 win.redrawOpenGlSceneNow(); 1940 } 1941 1942 void redrawOpenGlScene(void delegate() dg) { 1943 win.redrawOpenGlScene = dg; 1944 } 1945 1946 override void showing(bool s, bool recalc) { 1947 auto cur = hidden; 1948 win.hidden = !s; 1949 if(cur != s && s) 1950 redraw(); 1951 } 1952 1953 /// OpenGL widgets cannot have child widgets. Do not call this. 1954 /* @disable */ final override void addChild(Widget, int) { 1955 throw new Error("cannot add children to OpenGL widgets"); 1956 } 1957 1958 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 1959 /// Keep in mind that events like mouse coordinates are still relative to your size. 1960 override void registerMovement() { 1961 //import std.stdio; writefln("%d %d %d %d", x,y,width,height); 1962 version(win32_widgets) 1963 auto pos = getChildPositionRelativeToParentHwnd(this); 1964 else 1965 auto pos = getChildPositionRelativeToParentOrigin(this); 1966 win.moveResize(pos[0], pos[1], width, height); 1967 1968 win.setAsCurrentOpenGlContext(); 1969 sendResizeEvent(); 1970 } 1971 1972 //void delegate() drawFrame; 1973 } 1974 1975 /++ 1976 1977 +/ 1978 version(custom_widgets) 1979 class ListWidget : ScrollableWidget { 1980 1981 static struct Option { 1982 string label; 1983 bool selected; 1984 } 1985 1986 void setSelection(int y) { 1987 if(!multiSelect) 1988 foreach(ref opt; options) 1989 opt.selected = false; 1990 if(y >= 0 && y < options.length) 1991 options[y].selected = !options[y].selected; 1992 1993 auto evt = new Event(EventType.change, this); 1994 evt.dispatch(); 1995 1996 redraw(); 1997 1998 } 1999 2000 override void defaultEventHandler_click(Event event) { 2001 this.focus(); 2002 auto y = (event.clientY - 4) / Window.lineHeight; 2003 if(y >= 0 && y < options.length) { 2004 setSelection(y); 2005 } 2006 super.defaultEventHandler_click(event); 2007 } 2008 2009 this(Widget parent = null) { 2010 tabStop = false; 2011 super(parent); 2012 } 2013 2014 override void paintFrameAndBackground(WidgetPainter painter) { 2015 draw3dFrame(this, painter, FrameStyle.sunk, Color.white); 2016 } 2017 2018 override void paint(WidgetPainter painter) { 2019 auto pos = Point(4, 4); 2020 foreach(idx, option; options) { 2021 painter.fillColor = Color.white; 2022 painter.outlineColor = Color.white; 2023 painter.drawRectangle(pos, width - 8, Window.lineHeight); 2024 painter.outlineColor = Color.black; 2025 painter.drawText(pos, option.label); 2026 if(option.selected) { 2027 painter.rasterOp = RasterOp.xor; 2028 painter.outlineColor = Color.white; 2029 painter.fillColor = activeListXorColor; 2030 painter.drawRectangle(pos, width - 8, Window.lineHeight); 2031 painter.rasterOp = RasterOp.normal; 2032 } 2033 pos.y += Window.lineHeight; 2034 } 2035 } 2036 2037 2038 void addOption(string text) { 2039 options ~= Option(text); 2040 setContentSize(width, cast(int) (options.length * Window.lineHeight)); 2041 redraw(); 2042 } 2043 2044 void clear() { 2045 options = null; 2046 redraw(); 2047 } 2048 2049 Option[] options; 2050 bool multiSelect; 2051 2052 override int heightStretchiness() { return 6; } 2053 } 2054 2055 2056 2057 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 2058 enum ScrollBarShowPolicy { 2059 automatic, /// automatically show the scroll bar if it is necessary 2060 never, /// never show the scroll bar (scrolling must be done programmatically) 2061 always /// always show the scroll bar, even if it is disabled 2062 } 2063 2064 /++ 2065 FIXME ScrollBarShowPolicy 2066 FIXME: use the ScrollMessageWidget in here now that it exists 2067 +/ 2068 class ScrollableWidget : Widget { 2069 // FIXME: make line size configurable 2070 // FIXME: add keyboard controls 2071 version(win32_widgets) { 2072 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 2073 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 2074 auto pos = HIWORD(wParam); 2075 auto m = LOWORD(wParam); 2076 2077 // FIXME: I can reintroduce the 2078 // scroll bars now by using this 2079 // in the top-level window handler 2080 // to forward comamnds 2081 auto scrollbarHwnd = lParam; 2082 switch(m) { 2083 case SB_BOTTOM: 2084 if(msg == WM_HSCROLL) 2085 horizontalScrollTo(contentWidth_); 2086 else 2087 verticalScrollTo(contentHeight_); 2088 break; 2089 case SB_TOP: 2090 if(msg == WM_HSCROLL) 2091 horizontalScrollTo(0); 2092 else 2093 verticalScrollTo(0); 2094 break; 2095 case SB_ENDSCROLL: 2096 // idk 2097 break; 2098 case SB_LINEDOWN: 2099 if(msg == WM_HSCROLL) 2100 horizontalScroll(16); 2101 else 2102 verticalScroll(16); 2103 break; 2104 case SB_LINEUP: 2105 if(msg == WM_HSCROLL) 2106 horizontalScroll(-16); 2107 else 2108 verticalScroll(-16); 2109 break; 2110 case SB_PAGEDOWN: 2111 if(msg == WM_HSCROLL) 2112 horizontalScroll(100); 2113 else 2114 verticalScroll(100); 2115 break; 2116 case SB_PAGEUP: 2117 if(msg == WM_HSCROLL) 2118 horizontalScroll(-100); 2119 else 2120 verticalScroll(-100); 2121 break; 2122 case SB_THUMBPOSITION: 2123 case SB_THUMBTRACK: 2124 if(msg == WM_HSCROLL) 2125 horizontalScrollTo(pos); 2126 else 2127 verticalScrollTo(pos); 2128 2129 if(m == SB_THUMBTRACK) { 2130 // the event loop doesn't seem to carry on with a requested redraw.. 2131 // so we request it to get our dirty bit set... 2132 redraw(); 2133 // then we need to immediately actually redraw it too for instant feedback to user 2134 actualRedraw(); 2135 } 2136 break; 2137 default: 2138 } 2139 } 2140 return 0; 2141 } 2142 } 2143 /// 2144 this(Widget parent) { 2145 this.parentWindow = parent.parentWindow; 2146 2147 version(win32_widgets) { 2148 static bool classRegistered = false; 2149 if(!classRegistered) { 2150 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 2151 WNDCLASSEX wc; 2152 wc.cbSize = wc.sizeof; 2153 wc.hInstance = hInstance; 2154 wc.lpfnWndProc = &DefWindowProc; 2155 wc.lpszClassName = "arsd_minigui_ScrollableWidget"w.ptr; 2156 if(!RegisterClassExW(&wc)) 2157 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 2158 classRegistered = true; 2159 } 2160 2161 createWin32Window(this, "arsd_minigui_ScrollableWidget"w, "", 2162 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 2163 super(parent); 2164 } else version(custom_widgets) { 2165 outerContainer = new ScrollableContainerWidget(this, parent); 2166 super(outerContainer); 2167 } else static assert(0); 2168 } 2169 2170 version(custom_widgets) 2171 ScrollableContainerWidget outerContainer; 2172 2173 override void defaultEventHandler_click(Event event) { 2174 if(event.button == MouseButton.wheelUp) 2175 verticalScroll(-16); 2176 if(event.button == MouseButton.wheelDown) 2177 verticalScroll(16); 2178 super.defaultEventHandler_click(event); 2179 } 2180 2181 override void defaultEventHandler_keydown(Event event) { 2182 switch(event.key) { 2183 case Key.Left: 2184 horizontalScroll(-16); 2185 break; 2186 case Key.Right: 2187 horizontalScroll(16); 2188 break; 2189 case Key.Up: 2190 verticalScroll(-16); 2191 break; 2192 case Key.Down: 2193 verticalScroll(16); 2194 break; 2195 case Key.Home: 2196 verticalScrollTo(0); 2197 break; 2198 case Key.End: 2199 verticalScrollTo(contentHeight); 2200 break; 2201 case Key.PageUp: 2202 verticalScroll(-160); 2203 break; 2204 case Key.PageDown: 2205 verticalScroll(160); 2206 break; 2207 default: 2208 } 2209 super.defaultEventHandler_keydown(event); 2210 } 2211 2212 2213 version(win32_widgets) 2214 override void recomputeChildLayout() { 2215 super.recomputeChildLayout(); 2216 SCROLLINFO info; 2217 info.cbSize = info.sizeof; 2218 info.nPage = viewportHeight; 2219 info.fMask = SIF_PAGE | SIF_RANGE; 2220 info.nMin = 0; 2221 info.nMax = contentHeight_; 2222 SetScrollInfo(hwnd, SB_VERT, &info, true); 2223 2224 info.cbSize = info.sizeof; 2225 info.nPage = viewportWidth; 2226 info.fMask = SIF_PAGE | SIF_RANGE; 2227 info.nMin = 0; 2228 info.nMax = contentWidth_; 2229 SetScrollInfo(hwnd, SB_HORZ, &info, true); 2230 } 2231 2232 2233 2234 /* 2235 Scrolling 2236 ------------ 2237 2238 You are assigned a width and a height by the layout engine, which 2239 is your viewport box. However, you may draw more than that by setting 2240 a contentWidth and contentHeight. 2241 2242 If these can be contained by the viewport, no scrollbar is displayed. 2243 If they cannot fit though, it will automatically show scroll as necessary. 2244 2245 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 2246 is zero, no vertical scrolling is performed. 2247 2248 If scrolling is necessary, the lib will automatically work with the bars. 2249 When you redraw, the origin and clipping info in the painter is set so if 2250 you just draw everything, it will work, but you can be more efficient by checking 2251 the viewportWidth, viewportHeight, and scrollOrigin members. 2252 */ 2253 2254 /// 2255 final @property int viewportWidth() { 2256 return width - (showingVerticalScroll ? 16 : 0); 2257 } 2258 /// 2259 final @property int viewportHeight() { 2260 return height - (showingHorizontalScroll ? 16 : 0); 2261 } 2262 2263 // FIXME property 2264 Point scrollOrigin_; 2265 2266 /// 2267 final const(Point) scrollOrigin() { 2268 return scrollOrigin_; 2269 } 2270 2271 // the user sets these two 2272 private int contentWidth_ = 0; 2273 private int contentHeight_ = 0; 2274 2275 /// 2276 int contentWidth() { return contentWidth_; } 2277 /// 2278 int contentHeight() { return contentHeight_; } 2279 2280 /// 2281 void setContentSize(int width, int height) { 2282 contentWidth_ = width; 2283 contentHeight_ = height; 2284 2285 version(custom_widgets) { 2286 if(showingVerticalScroll || showingHorizontalScroll) { 2287 outerContainer.recomputeChildLayout(); 2288 } 2289 2290 if(showingVerticalScroll()) 2291 outerContainer.verticalScrollBar.redraw(); 2292 if(showingHorizontalScroll()) 2293 outerContainer.horizontalScrollBar.redraw(); 2294 } else version(win32_widgets) { 2295 recomputeChildLayout(); 2296 } else static assert(0); 2297 } 2298 2299 /// 2300 void verticalScroll(int delta) { 2301 verticalScrollTo(scrollOrigin.y + delta); 2302 } 2303 /// 2304 void verticalScrollTo(int pos) { 2305 scrollOrigin_.y = pos; 2306 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 2307 scrollOrigin_.y = contentHeight - viewportHeight; 2308 2309 if(scrollOrigin_.y < 0) 2310 scrollOrigin_.y = 0; 2311 2312 version(win32_widgets) { 2313 SCROLLINFO info; 2314 info.cbSize = info.sizeof; 2315 info.fMask = SIF_POS; 2316 info.nPos = scrollOrigin_.y; 2317 SetScrollInfo(hwnd, SB_VERT, &info, true); 2318 } else version(custom_widgets) { 2319 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 2320 } else static assert(0); 2321 2322 redraw(); 2323 } 2324 2325 /// 2326 void horizontalScroll(int delta) { 2327 horizontalScrollTo(scrollOrigin.x + delta); 2328 } 2329 /// 2330 void horizontalScrollTo(int pos) { 2331 scrollOrigin_.x = pos; 2332 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 2333 scrollOrigin_.x = contentWidth - viewportWidth; 2334 2335 if(scrollOrigin_.x < 0) 2336 scrollOrigin_.x = 0; 2337 2338 version(win32_widgets) { 2339 SCROLLINFO info; 2340 info.cbSize = info.sizeof; 2341 info.fMask = SIF_POS; 2342 info.nPos = scrollOrigin_.x; 2343 SetScrollInfo(hwnd, SB_HORZ, &info, true); 2344 } else version(custom_widgets) { 2345 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 2346 } else static assert(0); 2347 2348 redraw(); 2349 } 2350 /// 2351 void scrollTo(Point p) { 2352 verticalScrollTo(p.y); 2353 horizontalScrollTo(p.x); 2354 } 2355 2356 /// 2357 void ensureVisibleInScroll(Point p) { 2358 auto rect = viewportRectangle(); 2359 if(rect.contains(p)) 2360 return; 2361 if(p.x < rect.left) 2362 horizontalScroll(p.x - rect.left); 2363 else if(p.x > rect.right) 2364 horizontalScroll(p.x - rect.right); 2365 2366 if(p.y < rect.top) 2367 verticalScroll(p.y - rect.top); 2368 else if(p.y > rect.bottom) 2369 verticalScroll(p.y - rect.bottom); 2370 } 2371 2372 /// 2373 void ensureVisibleInScroll(Rectangle rect) { 2374 ensureVisibleInScroll(rect.upperLeft); 2375 ensureVisibleInScroll(rect.lowerRight); 2376 } 2377 2378 /// 2379 Rectangle viewportRectangle() { 2380 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 2381 } 2382 2383 /// 2384 bool showingHorizontalScroll() { 2385 return contentWidth > width; 2386 } 2387 /// 2388 bool showingVerticalScroll() { 2389 return contentHeight > height; 2390 } 2391 2392 /// This is called before the ordinary paint delegate, 2393 /// giving you a chance to draw the window frame, etc, 2394 /// before the scroll clip takes effect 2395 void paintFrameAndBackground(WidgetPainter painter) { 2396 version(win32_widgets) { 2397 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 2398 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 2399 // since the pen is null, to fill the whole space, we need the +1 on both. 2400 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 2401 SelectObject(painter.impl.hdc, p); 2402 SelectObject(painter.impl.hdc, b); 2403 } 2404 2405 } 2406 2407 // make space for the scroll bar, and that's it. 2408 final override int paddingRight() { return 16; } 2409 final override int paddingBottom() { return 16; } 2410 2411 /* 2412 END SCROLLING 2413 */ 2414 2415 override WidgetPainter draw() { 2416 int x = this.x, y = this.y; 2417 auto parent = this.parent; 2418 while(parent) { 2419 x += parent.x; 2420 y += parent.y; 2421 parent = parent.parent; 2422 } 2423 2424 //version(win32_widgets) { 2425 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw() : parentWindow.win.draw(); 2426 //} else { 2427 auto painter = parentWindow.win.draw(); 2428 //} 2429 painter.originX = x; 2430 painter.originY = y; 2431 2432 painter.originX = painter.originX - scrollOrigin.x; 2433 painter.originY = painter.originY - scrollOrigin.y; 2434 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 2435 2436 return WidgetPainter(painter); 2437 } 2438 2439 override protected void privatePaint(WidgetPainter painter, int lox, int loy, bool force = false) { 2440 if(hidden) 2441 return; 2442 2443 //version(win32_widgets) 2444 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw() : parentWindow.win.draw(); 2445 2446 painter.originX = lox + x; 2447 painter.originY = loy + y; 2448 2449 bool actuallyPainted = false; 2450 2451 if(force || redrawRequested) { 2452 painter.setClipRectangle(Point(0, 0), width, height); 2453 paintFrameAndBackground(painter); 2454 } 2455 2456 painter.originX = painter.originX - scrollOrigin.x; 2457 painter.originY = painter.originY - scrollOrigin.y; 2458 if(force || redrawRequested) { 2459 painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 2460 2461 //erase(painter); // we paintFrameAndBackground above so no need 2462 paint(painter); 2463 2464 actuallyPainted = true; 2465 redrawRequested = false; 2466 } 2467 foreach(child; children) { 2468 if(cast(FixedPosition) child) 2469 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, actuallyPainted); 2470 else 2471 child.privatePaint(painter, painter.originX, painter.originY, actuallyPainted); 2472 } 2473 } 2474 } 2475 2476 version(custom_widgets) 2477 private class ScrollableContainerWidget : Widget { 2478 2479 ScrollableWidget sw; 2480 2481 VerticalScrollbar verticalScrollBar; 2482 HorizontalScrollbar horizontalScrollBar; 2483 2484 this(ScrollableWidget sw, Widget parent) { 2485 this.sw = sw; 2486 2487 this.tabStop = false; 2488 2489 horizontalScrollBar = new HorizontalScrollbar(this); 2490 verticalScrollBar = new VerticalScrollbar(this); 2491 2492 horizontalScrollBar.showing_ = false; 2493 verticalScrollBar.showing_ = false; 2494 2495 horizontalScrollBar.addEventListener("scrolltonextline", { 2496 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 2497 sw.horizontalScrollTo(horizontalScrollBar.position); 2498 }); 2499 horizontalScrollBar.addEventListener("scrolltopreviousline", { 2500 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 2501 sw.horizontalScrollTo(horizontalScrollBar.position); 2502 }); 2503 verticalScrollBar.addEventListener("scrolltonextline", { 2504 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 2505 sw.verticalScrollTo(verticalScrollBar.position); 2506 }); 2507 verticalScrollBar.addEventListener("scrolltopreviousline", { 2508 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 2509 sw.verticalScrollTo(verticalScrollBar.position); 2510 }); 2511 horizontalScrollBar.addEventListener("scrolltonextpage", { 2512 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 2513 sw.horizontalScrollTo(horizontalScrollBar.position); 2514 }); 2515 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 2516 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 2517 sw.horizontalScrollTo(horizontalScrollBar.position); 2518 }); 2519 verticalScrollBar.addEventListener("scrolltonextpage", { 2520 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 2521 sw.verticalScrollTo(verticalScrollBar.position); 2522 }); 2523 verticalScrollBar.addEventListener("scrolltopreviouspage", { 2524 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 2525 sw.verticalScrollTo(verticalScrollBar.position); 2526 }); 2527 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 2528 horizontalScrollBar.setPosition(event.intValue); 2529 sw.horizontalScrollTo(horizontalScrollBar.position); 2530 }); 2531 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 2532 verticalScrollBar.setPosition(event.intValue); 2533 sw.verticalScrollTo(verticalScrollBar.position); 2534 }); 2535 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 2536 horizontalScrollBar.setPosition(event.intValue); 2537 sw.horizontalScrollTo(horizontalScrollBar.position); 2538 }); 2539 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 2540 verticalScrollBar.setPosition(event.intValue); 2541 }); 2542 2543 super(parent); 2544 } 2545 2546 // this is supposed to be basically invisible... 2547 override int minWidth() { return sw.minWidth; } 2548 override int minHeight() { return sw.minHeight; } 2549 override int maxWidth() { return sw.maxWidth; } 2550 override int maxHeight() { return sw.maxHeight; } 2551 override int widthStretchiness() { return sw.widthStretchiness; } 2552 override int heightStretchiness() { return sw.heightStretchiness; } 2553 override int marginLeft() { return sw.marginLeft; } 2554 override int marginRight() { return sw.marginRight; } 2555 override int marginTop() { return sw.marginTop; } 2556 override int marginBottom() { return sw.marginBottom; } 2557 override int paddingLeft() { return sw.paddingLeft; } 2558 override int paddingRight() { return sw.paddingRight; } 2559 override int paddingTop() { return sw.paddingTop; } 2560 override int paddingBottom() { return sw.paddingBottom; } 2561 override void focus() { sw.focus(); } 2562 2563 2564 override void recomputeChildLayout() { 2565 if(sw is null) return; 2566 2567 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 2568 if(horizontalScrollBar && verticalScrollBar) { 2569 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 2570 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 2571 horizontalScrollBar.x = 0; 2572 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 2573 2574 verticalScrollBar.width = verticalScrollBar.minWidth(); 2575 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 2576 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 2577 verticalScrollBar.y = 0 + 2; 2578 2579 sw.x = 0; 2580 sw.y = 0; 2581 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 2582 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 2583 2584 if(sw.contentWidth_ <= this.width) 2585 sw.scrollOrigin_.x = 0; 2586 if(sw.contentHeight_ <= this.height) 2587 sw.scrollOrigin_.y = 0; 2588 2589 horizontalScrollBar.recomputeChildLayout(); 2590 verticalScrollBar.recomputeChildLayout(); 2591 sw.recomputeChildLayout(); 2592 } 2593 2594 if(sw.contentWidth_ <= this.width) 2595 sw.scrollOrigin_.x = 0; 2596 if(sw.contentHeight_ <= this.height) 2597 sw.scrollOrigin_.y = 0; 2598 2599 if(sw.showingHorizontalScroll()) 2600 horizontalScrollBar.showing = true; 2601 else 2602 horizontalScrollBar.showing = false; 2603 if(sw.showingVerticalScroll()) 2604 verticalScrollBar.showing = true; 2605 else 2606 verticalScrollBar.showing = false; 2607 2608 2609 verticalScrollBar.setViewableArea(sw.viewportHeight()); 2610 verticalScrollBar.setMax(sw.contentHeight); 2611 verticalScrollBar.setPosition(sw.scrollOrigin.y); 2612 2613 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 2614 horizontalScrollBar.setMax(sw.contentWidth); 2615 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 2616 } 2617 } 2618 2619 /* 2620 class ScrollableClientWidget : Widget { 2621 this(Widget parent) { 2622 super(parent); 2623 } 2624 override void paint(WidgetPainter p) { 2625 parent.paint(p); 2626 } 2627 } 2628 */ 2629 2630 /// 2631 abstract class ScrollbarBase : Widget { 2632 /// 2633 this(Widget parent) { 2634 super(parent); 2635 tabStop = false; 2636 } 2637 2638 private int viewableArea_; 2639 private int max_; 2640 private int step_ = 16; 2641 private int position_; 2642 2643 /// 2644 bool atEnd() { 2645 return position_ + viewableArea_ >= max_; 2646 } 2647 2648 /// 2649 bool atStart() { 2650 return position_ == 0; 2651 } 2652 2653 /// 2654 void setViewableArea(int a) { 2655 viewableArea_ = a; 2656 version(custom_widgets) 2657 redraw(); 2658 } 2659 /// 2660 void setMax(int a) { 2661 max_ = a; 2662 version(custom_widgets) 2663 redraw(); 2664 } 2665 /// 2666 int max() { 2667 return max_; 2668 } 2669 /// 2670 void setPosition(int a) { 2671 if(a == int.max) 2672 a = max; 2673 position_ = max ? a : 0; 2674 if(position_ + viewableArea_ > max) 2675 position_ = max - viewableArea_; 2676 if(position_ < 0) 2677 position_ = 0; 2678 version(custom_widgets) 2679 redraw(); 2680 } 2681 /// 2682 int position() { 2683 return position_; 2684 } 2685 /// 2686 void setStep(int a) { 2687 step_ = a; 2688 } 2689 /// 2690 int step() { 2691 return step_; 2692 } 2693 2694 // FIXME: remove this.... maybe 2695 protected void informProgramThatUserChangedPosition(int n) { 2696 position_ = n; 2697 auto evt = new Event(EventType.change, this); 2698 evt.intValue = n; 2699 evt.dispatch(); 2700 } 2701 2702 version(custom_widgets) { 2703 abstract protected int getBarDim(); 2704 int thumbSize() { 2705 if(viewableArea_ >= max_) 2706 return getBarDim(); 2707 2708 int res; 2709 if(max_) { 2710 res = getBarDim() * viewableArea_ / max_; 2711 } 2712 if(res < 6) 2713 res = 6; 2714 2715 return res; 2716 } 2717 2718 int thumbPosition() { 2719 /* 2720 viewableArea_ is the viewport height/width 2721 position_ is where we are 2722 */ 2723 if(max_) { 2724 if(position_ + viewableArea_ >= max_) 2725 return getBarDim - thumbSize; 2726 return getBarDim * position_ / max_; 2727 } 2728 return 0; 2729 } 2730 } 2731 } 2732 2733 //public import mgt; 2734 2735 /++ 2736 A mouse tracking widget is one that follows the mouse when dragged inside it. 2737 2738 Concrete subclasses may include a scrollbar thumb and a volume control. 2739 +/ 2740 //version(custom_widgets) 2741 class MouseTrackingWidget : Widget { 2742 2743 /// 2744 int positionX() { return positionX_; } 2745 /// 2746 int positionY() { return positionY_; } 2747 2748 /// 2749 void positionX(int p) { positionX_ = p; } 2750 /// 2751 void positionY(int p) { positionY_ = p; } 2752 2753 private int positionX_; 2754 private int positionY_; 2755 2756 /// 2757 enum Orientation { 2758 horizontal, /// 2759 vertical, /// 2760 twoDimensional, /// 2761 } 2762 2763 private int thumbWidth_; 2764 private int thumbHeight_; 2765 2766 /// 2767 int thumbWidth() { return thumbWidth_; } 2768 /// 2769 int thumbHeight() { return thumbHeight_; } 2770 /// 2771 int thumbWidth(int a) { return thumbWidth_ = a; } 2772 /// 2773 int thumbHeight(int a) { return thumbHeight_ = a; } 2774 2775 private bool dragging; 2776 private bool hovering; 2777 private int startMouseX, startMouseY; 2778 2779 /// 2780 this(Orientation orientation, Widget parent = null) { 2781 super(parent); 2782 2783 //assert(parentWindow !is null); 2784 2785 addEventListener(EventType.mousedown, (Event event) { 2786 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 2787 dragging = true; 2788 startMouseX = event.clientX - positionX; 2789 startMouseY = event.clientY - positionY; 2790 parentWindow.captureMouse(this); 2791 } else { 2792 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 2793 positionX = event.clientX - thumbWidth / 2; 2794 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 2795 positionY = event.clientY - thumbHeight / 2; 2796 2797 if(positionX + thumbWidth > this.width) 2798 positionX = this.width - thumbWidth; 2799 if(positionY + thumbHeight > this.height) 2800 positionY = this.height - thumbHeight; 2801 2802 if(positionX < 0) 2803 positionX = 0; 2804 if(positionY < 0) 2805 positionY = 0; 2806 2807 2808 auto evt = new Event(EventType.change, this); 2809 evt.sendDirectly(); 2810 2811 redraw(); 2812 2813 } 2814 }); 2815 2816 addEventListener(EventType.mouseup, (Event event) { 2817 dragging = false; 2818 parentWindow.releaseMouseCapture(); 2819 }); 2820 2821 addEventListener(EventType.mouseout, (Event event) { 2822 if(!hovering) 2823 return; 2824 hovering = false; 2825 redraw(); 2826 }); 2827 2828 int lpx, lpy; 2829 2830 addEventListener(EventType.mousemove, (Event event) { 2831 auto oh = hovering; 2832 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 2833 hovering = true; 2834 } else { 2835 hovering = false; 2836 } 2837 if(!dragging) { 2838 if(hovering != oh) 2839 redraw(); 2840 return; 2841 } 2842 2843 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 2844 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 2845 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 2846 positionY = event.clientY - startMouseY; 2847 2848 if(positionX + thumbWidth > this.width) 2849 positionX = this.width - thumbWidth; 2850 if(positionY + thumbHeight > this.height) 2851 positionY = this.height - thumbHeight; 2852 2853 if(positionX < 0) 2854 positionX = 0; 2855 if(positionY < 0) 2856 positionY = 0; 2857 2858 if(positionX != lpx || positionY != lpy) { 2859 auto evt = new Event(EventType.change, this); 2860 evt.sendDirectly(); 2861 2862 lpx = positionX; 2863 lpy = positionY; 2864 } 2865 2866 redraw(); 2867 }); 2868 } 2869 2870 version(custom_widgets) 2871 override void paint(WidgetPainter painter) { 2872 auto c = darken(windowBackgroundColor, 0.2); 2873 painter.outlineColor = c; 2874 painter.fillColor = c; 2875 painter.drawRectangle(Point(0, 0), this.width, this.height); 2876 2877 auto color = hovering ? hoveringColor : windowBackgroundColor; 2878 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 2879 } 2880 } 2881 2882 //version(custom_widgets) 2883 //private 2884 class HorizontalScrollbar : ScrollbarBase { 2885 2886 version(custom_widgets) { 2887 private MouseTrackingWidget thumb; 2888 2889 override int getBarDim() { 2890 return thumb.width; 2891 } 2892 } 2893 2894 override void setViewableArea(int a) { 2895 super.setViewableArea(a); 2896 2897 version(win32_widgets) { 2898 SCROLLINFO info; 2899 info.cbSize = info.sizeof; 2900 info.nPage = a + 1; 2901 info.fMask = SIF_PAGE; 2902 SetScrollInfo(hwnd, SB_CTL, &info, true); 2903 } else version(custom_widgets) { 2904 thumb.positionX = thumbPosition; 2905 thumb.thumbWidth = thumbSize; 2906 thumb.redraw(); 2907 } else static assert(0); 2908 2909 } 2910 2911 override void setMax(int a) { 2912 super.setMax(a); 2913 version(win32_widgets) { 2914 SCROLLINFO info; 2915 info.cbSize = info.sizeof; 2916 info.nMin = 0; 2917 info.nMax = max; 2918 info.fMask = SIF_RANGE; 2919 SetScrollInfo(hwnd, SB_CTL, &info, true); 2920 } else version(custom_widgets) { 2921 thumb.positionX = thumbPosition; 2922 thumb.thumbWidth = thumbSize; 2923 thumb.redraw(); 2924 } 2925 } 2926 2927 override void setPosition(int a) { 2928 super.setPosition(a); 2929 version(win32_widgets) { 2930 SCROLLINFO info; 2931 info.cbSize = info.sizeof; 2932 info.fMask = SIF_POS; 2933 info.nPos = position; 2934 SetScrollInfo(hwnd, SB_CTL, &info, true); 2935 } else version(custom_widgets) { 2936 thumb.positionX = thumbPosition(); 2937 thumb.thumbWidth = thumbSize; 2938 thumb.redraw(); 2939 } else static assert(0); 2940 } 2941 2942 this(Widget parent) { 2943 super(parent); 2944 2945 version(win32_widgets) { 2946 createWin32Window(this, "Scrollbar"w, "", 2947 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 2948 } else version(custom_widgets) { 2949 auto vl = new HorizontalLayout(this); 2950 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 2951 leftButton.setClickRepeat(scrollClickRepeatInterval); 2952 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 2953 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 2954 rightButton.setClickRepeat(scrollClickRepeatInterval); 2955 2956 leftButton.tabStop = false; 2957 rightButton.tabStop = false; 2958 thumb.tabStop = false; 2959 2960 leftButton.addEventListener(EventType.triggered, () { 2961 auto ev = new Event("scrolltopreviousline", this); 2962 ev.dispatch(); 2963 //informProgramThatUserChangedPosition(position - step()); 2964 }); 2965 rightButton.addEventListener(EventType.triggered, () { 2966 auto ev = new Event("scrolltonextline", this); 2967 ev.dispatch(); 2968 //informProgramThatUserChangedPosition(position + step()); 2969 }); 2970 2971 thumb.thumbWidth = this.minWidth; 2972 thumb.thumbHeight = 16; 2973 2974 thumb.addEventListener(EventType.change, () { 2975 auto sx = thumb.positionX * max() / thumb.width; 2976 //informProgramThatUserChangedPosition(sx); 2977 2978 auto ev = new Event("scrolltoposition", this); 2979 ev.intValue = sx; 2980 ev.dispatch(); 2981 }); 2982 } 2983 } 2984 2985 override int minHeight() { return 16; } 2986 override int maxHeight() { return 16; } 2987 override int minWidth() { return 48; } 2988 } 2989 2990 //version(custom_widgets) 2991 //private 2992 class VerticalScrollbar : ScrollbarBase { 2993 2994 version(custom_widgets) { 2995 override int getBarDim() { 2996 return thumb.height; 2997 } 2998 2999 private MouseTrackingWidget thumb; 3000 } 3001 3002 override void setViewableArea(int a) { 3003 super.setViewableArea(a); 3004 3005 version(win32_widgets) { 3006 SCROLLINFO info; 3007 info.cbSize = info.sizeof; 3008 info.nPage = a + 1; 3009 info.fMask = SIF_PAGE; 3010 SetScrollInfo(hwnd, SB_CTL, &info, true); 3011 } else version(custom_widgets) { 3012 thumb.positionY = thumbPosition; 3013 thumb.thumbHeight = thumbSize; 3014 thumb.redraw(); 3015 } else static assert(0); 3016 3017 } 3018 3019 override void setMax(int a) { 3020 super.setMax(a); 3021 version(win32_widgets) { 3022 SCROLLINFO info; 3023 info.cbSize = info.sizeof; 3024 info.nMin = 0; 3025 info.nMax = max; 3026 info.fMask = SIF_RANGE; 3027 SetScrollInfo(hwnd, SB_CTL, &info, true); 3028 } else version(custom_widgets) { 3029 thumb.positionY = thumbPosition; 3030 thumb.thumbHeight = thumbSize; 3031 thumb.redraw(); 3032 } 3033 } 3034 3035 override void setPosition(int a) { 3036 super.setPosition(a); 3037 version(win32_widgets) { 3038 SCROLLINFO info; 3039 info.cbSize = info.sizeof; 3040 info.fMask = SIF_POS; 3041 info.nPos = position; 3042 SetScrollInfo(hwnd, SB_CTL, &info, true); 3043 } else version(custom_widgets) { 3044 thumb.positionY = thumbPosition; 3045 thumb.thumbHeight = thumbSize; 3046 thumb.redraw(); 3047 } else static assert(0); 3048 } 3049 3050 this(Widget parent) { 3051 super(parent); 3052 3053 version(win32_widgets) { 3054 createWin32Window(this, "Scrollbar"w, "", 3055 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 3056 } else version(custom_widgets) { 3057 auto vl = new VerticalLayout(this); 3058 auto upButton = new ArrowButton(ArrowDirection.up, vl); 3059 upButton.setClickRepeat(scrollClickRepeatInterval); 3060 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 3061 auto downButton = new ArrowButton(ArrowDirection.down, vl); 3062 downButton.setClickRepeat(scrollClickRepeatInterval); 3063 3064 upButton.addEventListener(EventType.triggered, () { 3065 auto ev = new Event("scrolltopreviousline", this); 3066 ev.dispatch(); 3067 //informProgramThatUserChangedPosition(position - step()); 3068 }); 3069 downButton.addEventListener(EventType.triggered, () { 3070 auto ev = new Event("scrolltonextline", this); 3071 ev.dispatch(); 3072 //informProgramThatUserChangedPosition(position + step()); 3073 }); 3074 3075 thumb.thumbWidth = this.minWidth; 3076 thumb.thumbHeight = 16; 3077 3078 thumb.addEventListener(EventType.change, () { 3079 auto sy = thumb.positionY * max() / thumb.height; 3080 3081 auto ev = new Event("scrolltoposition", this); 3082 ev.intValue = sy; 3083 ev.dispatch(); 3084 3085 //informProgramThatUserChangedPosition(sy); 3086 }); 3087 3088 upButton.tabStop = false; 3089 downButton.tabStop = false; 3090 thumb.tabStop = false; 3091 } 3092 } 3093 3094 override int minWidth() { return 16; } 3095 override int maxWidth() { return 16; } 3096 override int minHeight() { return 48; } 3097 } 3098 3099 3100 3101 /// 3102 abstract class Layout : Widget { 3103 this(Widget parent = null) { 3104 tabStop = false; 3105 super(parent); 3106 } 3107 } 3108 3109 /++ 3110 Makes all children minimum width and height, placing them down 3111 left to right, top to bottom. 3112 3113 Useful if you want to make a list of buttons that automatically 3114 wrap to a new line when necessary. 3115 +/ 3116 class InlineBlockLayout : Layout { 3117 /// 3118 this(Widget parent = null) { super(parent); } 3119 3120 override void recomputeChildLayout() { 3121 registerMovement(); 3122 3123 int x = this.paddingLeft, y = this.paddingTop; 3124 3125 int lineHeight; 3126 int previousMargin = 0; 3127 int previousMarginBottom = 0; 3128 3129 foreach(child; children) { 3130 if(child.hidden) 3131 continue; 3132 if(cast(FixedPosition) child) { 3133 child.recomputeChildLayout(); 3134 continue; 3135 } 3136 child.width = child.minWidth(); 3137 if(child.width == 0) 3138 child.width = 32; 3139 child.height = child.minHeight(); 3140 if(child.height == 0) 3141 child.height = 32; 3142 3143 if(x + child.width + paddingRight > this.width) { 3144 x = this.paddingLeft; 3145 y += lineHeight; 3146 lineHeight = 0; 3147 previousMargin = 0; 3148 previousMarginBottom = 0; 3149 } 3150 3151 auto margin = child.marginLeft; 3152 if(previousMargin > margin) 3153 margin = previousMargin; 3154 3155 x += margin; 3156 3157 child.x = x; 3158 child.y = y; 3159 3160 int marginTopApplied; 3161 if(child.marginTop > previousMarginBottom) { 3162 child.y += child.marginTop; 3163 marginTopApplied = child.marginTop; 3164 } 3165 3166 x += child.width; 3167 previousMargin = child.marginRight; 3168 3169 if(child.marginBottom > previousMarginBottom) 3170 previousMarginBottom = child.marginBottom; 3171 3172 auto h = child.height + previousMarginBottom + marginTopApplied; 3173 if(h > lineHeight) 3174 lineHeight = h; 3175 3176 child.recomputeChildLayout(); 3177 } 3178 3179 } 3180 3181 override int minWidth() { 3182 int min; 3183 foreach(child; children) { 3184 auto cm = child.minWidth; 3185 if(cm > min) 3186 min = cm; 3187 } 3188 return min + paddingLeft + paddingRight; 3189 } 3190 3191 override int minHeight() { 3192 int min; 3193 foreach(child; children) { 3194 auto cm = child.minHeight; 3195 if(cm > min) 3196 min = cm; 3197 } 3198 return min + paddingTop + paddingBottom; 3199 } 3200 } 3201 3202 /++ 3203 A tab widget is a set of clickable tab buttons followed by a content area. 3204 3205 3206 Tabs can change existing content or can be new pages. 3207 3208 When the user picks a different tab, a `change` message is generated. 3209 +/ 3210 class TabWidget : Widget { 3211 this(Widget parent) { 3212 super(parent); 3213 3214 version(win32_widgets) { 3215 createWin32Window(this, WC_TABCONTROL, "", 0); 3216 } else version(custom_widgets) { 3217 tabBarHeight = Window.lineHeight; 3218 3219 addDirectEventListener(EventType.click, (Event event) { 3220 if(event.clientY < tabBarHeight) { 3221 auto t = (event.clientX / tabWidth); 3222 if(t >= 0 && t < children.length) 3223 setCurrentTab(t); 3224 } 3225 }); 3226 } else static assert(0); 3227 } 3228 3229 override int marginTop() { return 4; } 3230 override int paddingBottom() { return 4; } 3231 3232 override int minHeight() { 3233 int max = 0; 3234 foreach(child; children) 3235 max = mymax(child.minHeight, max); 3236 3237 3238 version(win32_widgets) { 3239 RECT rect; 3240 rect.right = this.width; 3241 rect.bottom = max; 3242 TabCtrl_AdjustRect(hwnd, true, &rect); 3243 3244 max = rect.bottom; 3245 } else { 3246 max += Window.lineHeight + 4; 3247 } 3248 3249 3250 return max; 3251 } 3252 3253 version(win32_widgets) 3254 override int handleWmNotify(NMHDR* hdr, int code) { 3255 switch(code) { 3256 case TCN_SELCHANGE: 3257 auto sel = TabCtrl_GetCurSel(hwnd); 3258 showOnly(sel); 3259 break; 3260 default: 3261 } 3262 return 0; 3263 } 3264 3265 override void addChild(Widget child, int pos = int.max) { 3266 if(auto twp = cast(TabWidgetPage) child) { 3267 super.addChild(child, pos); 3268 if(pos == int.max) 3269 pos = cast(int) this.children.length - 1; 3270 3271 version(win32_widgets) { 3272 TCITEM item; 3273 item.mask = TCIF_TEXT; 3274 WCharzBuffer buf = WCharzBuffer(twp.title); 3275 item.pszText = buf.ptr; 3276 SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 3277 } else version(custom_widgets) { 3278 } 3279 3280 if(pos != getCurrentTab) { 3281 child.showing = false; 3282 } 3283 } else { 3284 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 3285 } 3286 } 3287 3288 override void recomputeChildLayout() { 3289 version(win32_widgets) { 3290 this.registerMovement(); 3291 3292 RECT rect; 3293 GetWindowRect(hwnd, &rect); 3294 3295 auto left = rect.left; 3296 auto top = rect.top; 3297 3298 TabCtrl_AdjustRect(hwnd, false, &rect); 3299 foreach(child; children) { 3300 child.x = rect.left - left; 3301 child.y = rect.top - top; 3302 child.width = rect.right - rect.left; 3303 child.height = rect.bottom - rect.top; 3304 child.recomputeChildLayout(); 3305 } 3306 } else version(custom_widgets) { 3307 this.registerMovement(); 3308 foreach(child; children) { 3309 child.x = 2; 3310 child.y = tabBarHeight + 2; // for the border 3311 child.width = width - 4; // for the border 3312 child.height = height - tabBarHeight - 2 - 2; // for the border 3313 child.recomputeChildLayout(); 3314 } 3315 } else static assert(0); 3316 } 3317 3318 version(custom_widgets) { 3319 private int currentTab_; 3320 int tabBarHeight; 3321 int tabWidth = 80; 3322 } 3323 3324 version(custom_widgets) 3325 override void paint(WidgetPainter painter) { 3326 3327 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen); 3328 3329 int posX = 0; 3330 foreach(idx, child; children) { 3331 if(auto twp = cast(TabWidgetPage) child) { 3332 auto isCurrent = idx == getCurrentTab(); 3333 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? windowBackgroundColor : darken(windowBackgroundColor, 0.1)); 3334 painter.outlineColor = Color.black; 3335 painter.drawText(Point(posX + 4, 2), twp.title); 3336 3337 if(isCurrent) { 3338 painter.outlineColor = windowBackgroundColor; 3339 painter.fillColor = Color.transparent; 3340 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 3341 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 3342 3343 painter.outlineColor = Color.white; 3344 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 3345 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 3346 painter.outlineColor = activeTabColor; 3347 painter.drawPixel(Point(posX, tabBarHeight - 1)); 3348 } 3349 3350 posX += tabWidth - 2; 3351 } 3352 } 3353 } 3354 3355 /// 3356 @scriptable 3357 void setCurrentTab(int item) { 3358 version(win32_widgets) 3359 TabCtrl_SetCurSel(hwnd, item); 3360 else version(custom_widgets) 3361 currentTab_ = item; 3362 else static assert(0); 3363 3364 showOnly(item); 3365 } 3366 3367 /// 3368 @scriptable 3369 int getCurrentTab() { 3370 version(win32_widgets) 3371 return TabCtrl_GetCurSel(hwnd); 3372 else version(custom_widgets) 3373 return currentTab_; // FIXME 3374 else static assert(0); 3375 } 3376 3377 /// 3378 @scriptable 3379 void removeTab(int item) { 3380 if(item && item == getCurrentTab()) 3381 setCurrentTab(item - 1); 3382 3383 version(win32_widgets) { 3384 TabCtrl_DeleteItem(hwnd, item); 3385 } 3386 3387 for(int a = item; a < children.length - 1; a++) 3388 this.children[a] = this.children[a + 1]; 3389 this.children = this.children[0 .. $-1]; 3390 } 3391 3392 /// 3393 @scriptable 3394 TabWidgetPage addPage(string title) { 3395 return new TabWidgetPage(title, this); 3396 } 3397 3398 private void showOnly(int item) { 3399 foreach(idx, child; children) { 3400 child.hide(); 3401 } 3402 3403 foreach(idx, child; children) { 3404 if(idx == item) { 3405 child.show(); 3406 recomputeChildLayout(); 3407 } 3408 } 3409 3410 version(win32_widgets) { 3411 InvalidateRect(parentWindow.hwnd, null, true); 3412 } 3413 } 3414 } 3415 3416 /++ 3417 A page widget is basically a tab widget with hidden tabs. 3418 3419 You add [TabWidgetPage]s to it. 3420 +/ 3421 class PageWidget : Widget { 3422 this(Widget parent) { 3423 super(parent); 3424 } 3425 3426 override int minHeight() { 3427 int max = 0; 3428 foreach(child; children) 3429 max = mymax(child.minHeight, max); 3430 3431 return max; 3432 } 3433 3434 3435 override void addChild(Widget child, int pos = int.max) { 3436 if(auto twp = cast(TabWidgetPage) child) { 3437 super.addChild(child, pos); 3438 if(pos == int.max) 3439 pos = cast(int) this.children.length - 1; 3440 3441 if(pos != getCurrentTab) { 3442 child.showing = false; 3443 } 3444 } else { 3445 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 3446 } 3447 } 3448 3449 override void recomputeChildLayout() { 3450 this.registerMovement(); 3451 foreach(child; children) { 3452 child.x = 0; 3453 child.y = 0; 3454 child.width = width; 3455 child.height = height; 3456 child.recomputeChildLayout(); 3457 } 3458 } 3459 3460 private int currentTab_; 3461 3462 /// 3463 @scriptable 3464 void setCurrentTab(int item) { 3465 currentTab_ = item; 3466 3467 showOnly(item); 3468 } 3469 3470 /// 3471 @scriptable 3472 int getCurrentTab() { 3473 return currentTab_; 3474 } 3475 3476 /// 3477 @scriptable 3478 void removeTab(int item) { 3479 if(item && item == getCurrentTab()) 3480 setCurrentTab(item - 1); 3481 3482 for(int a = item; a < children.length - 1; a++) 3483 this.children[a] = this.children[a + 1]; 3484 this.children = this.children[0 .. $-1]; 3485 } 3486 3487 /// 3488 @scriptable 3489 TabWidgetPage addPage(string title) { 3490 return new TabWidgetPage(title, this); 3491 } 3492 3493 private void showOnly(int item) { 3494 foreach(idx, child; children) 3495 if(idx == item) { 3496 child.show(); 3497 child.recomputeChildLayout(); 3498 } else { 3499 child.hide(); 3500 } 3501 } 3502 3503 } 3504 3505 /++ 3506 3507 +/ 3508 class TabWidgetPage : Widget { 3509 string title; 3510 this(string title, Widget parent) { 3511 this.title = title; 3512 super(parent); 3513 3514 ///* 3515 version(win32_widgets) { 3516 static bool classRegistered = false; 3517 if(!classRegistered) { 3518 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 3519 WNDCLASSEX wc; 3520 wc.cbSize = wc.sizeof; 3521 wc.hInstance = hInstance; 3522 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 3523 wc.lpfnWndProc = &DefWindowProc; 3524 wc.lpszClassName = "arsd_minigui_TabWidgetPage"w.ptr; 3525 if(!RegisterClassExW(&wc)) 3526 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 3527 classRegistered = true; 3528 } 3529 3530 3531 createWin32Window(this, "arsd_minigui_TabWidgetPage"w, "", 0); 3532 } 3533 //*/ 3534 } 3535 3536 override int minHeight() { 3537 int sum = 0; 3538 foreach(child; children) 3539 sum += child.minHeight(); 3540 return sum; 3541 } 3542 } 3543 3544 version(none) 3545 class CollapsableSidebar : Widget { 3546 3547 } 3548 3549 /// Stacks the widgets vertically, taking all the available width for each child. 3550 class VerticalLayout : Layout { 3551 // intentionally blank - widget's default is vertical layout right now 3552 /// 3553 this(Widget parent) { super(parent); } 3554 } 3555 3556 /// Stacks the widgets horizontally, taking all the available height for each child. 3557 class HorizontalLayout : Layout { 3558 /// 3559 this(Widget parent = null) { super(parent); } 3560 override void recomputeChildLayout() { 3561 .recomputeChildLayout!"width"(this); 3562 } 3563 3564 override int minHeight() { 3565 int largest = 0; 3566 int margins = 0; 3567 int lastMargin = 0; 3568 foreach(child; children) { 3569 auto mh = child.minHeight(); 3570 if(mh > largest) 3571 largest = mh; 3572 margins += mymax(lastMargin, child.marginTop()); 3573 lastMargin = child.marginBottom(); 3574 } 3575 return largest + margins; 3576 } 3577 3578 override int maxHeight() { 3579 int largest = 0; 3580 int margins = 0; 3581 int lastMargin = 0; 3582 foreach(child; children) { 3583 auto mh = child.maxHeight(); 3584 if(mh == int.max) 3585 return int.max; 3586 if(mh > largest) 3587 largest = mh; 3588 margins += mymax(lastMargin, child.marginTop()); 3589 lastMargin = child.marginBottom(); 3590 } 3591 return largest + margins; 3592 } 3593 3594 override int heightStretchiness() { 3595 int max; 3596 foreach(child; children) { 3597 auto c = child.heightStretchiness; 3598 if(c > max) 3599 max = c; 3600 } 3601 return max; 3602 } 3603 3604 } 3605 3606 /++ 3607 A widget that takes your widget, puts scroll bars around it, and sends 3608 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 3609 no effort to automatically scroll or clip its child widgets - it just sends 3610 the messages. 3611 +/ 3612 class ScrollMessageWidget : Widget { 3613 this(Widget parent = null) { 3614 super(parent); 3615 3616 container = new Widget(this); 3617 hsb = new HorizontalScrollbar(this); 3618 vsb = new VerticalScrollbar(this); 3619 3620 hsb.addEventListener("scrolltonextline", { 3621 hsb.setPosition(hsb.position + 1); 3622 notify(); 3623 }); 3624 hsb.addEventListener("scrolltopreviousline", { 3625 hsb.setPosition(hsb.position - 1); 3626 notify(); 3627 }); 3628 vsb.addEventListener("scrolltonextline", { 3629 vsb.setPosition(vsb.position + 1); 3630 notify(); 3631 }); 3632 vsb.addEventListener("scrolltopreviousline", { 3633 vsb.setPosition(vsb.position - 1); 3634 notify(); 3635 }); 3636 hsb.addEventListener("scrolltonextpage", { 3637 hsb.setPosition(hsb.position + hsb.step_); 3638 notify(); 3639 }); 3640 hsb.addEventListener("scrolltopreviouspage", { 3641 hsb.setPosition(hsb.position - hsb.step_); 3642 notify(); 3643 }); 3644 vsb.addEventListener("scrolltonextpage", { 3645 vsb.setPosition(vsb.position + vsb.step_); 3646 notify(); 3647 }); 3648 vsb.addEventListener("scrolltopreviouspage", { 3649 vsb.setPosition(vsb.position - vsb.step_); 3650 notify(); 3651 }); 3652 hsb.addEventListener("scrolltoposition", (Event event) { 3653 hsb.setPosition(event.intValue); 3654 notify(); 3655 }); 3656 vsb.addEventListener("scrolltoposition", (Event event) { 3657 vsb.setPosition(event.intValue); 3658 notify(); 3659 }); 3660 3661 3662 tabStop = false; 3663 container.tabStop = false; 3664 magic = true; 3665 } 3666 3667 /// 3668 void scrollUp() { 3669 vsb.setPosition(vsb.position - 1); 3670 notify(); 3671 } 3672 /// Ditto 3673 void scrollDown() { 3674 vsb.setPosition(vsb.position + 1); 3675 notify(); 3676 } 3677 3678 /// 3679 VerticalScrollbar verticalScrollBar() { return vsb; } 3680 /// 3681 HorizontalScrollbar horizontalScrollBar() { return hsb; } 3682 3683 void notify() { 3684 auto event = new Event("scroll", this); 3685 event.dispatch(); 3686 } 3687 3688 /// 3689 Point position() { 3690 return Point(hsb.position, vsb.position); 3691 } 3692 3693 /// 3694 void setPosition(int x, int y) { 3695 hsb.setPosition(x); 3696 vsb.setPosition(y); 3697 } 3698 3699 /// 3700 void setPageSize(int unitsX, int unitsY) { 3701 hsb.setStep(unitsX); 3702 vsb.setStep(unitsY); 3703 } 3704 3705 /// 3706 void setTotalArea(int width, int height) { 3707 hsb.setMax(width); 3708 vsb.setMax(height); 3709 } 3710 3711 /// 3712 void setViewableArea(int width, int height) { 3713 hsb.setViewableArea(width); 3714 vsb.setViewableArea(height); 3715 } 3716 3717 private bool magic; 3718 override void addChild(Widget w, int position = int.max) { 3719 if(magic) 3720 container.addChild(w, position); 3721 else 3722 super.addChild(w, position); 3723 } 3724 3725 override void recomputeChildLayout() { 3726 if(hsb is null || vsb is null || container is null) return; 3727 3728 registerMovement(); 3729 3730 hsb.height = 16; // FIXME? are tese 16s sane? 3731 hsb.x = 0; 3732 hsb.y = this.height - hsb.height; 3733 hsb.width = this.width - 16; 3734 hsb.recomputeChildLayout(); 3735 3736 vsb.width = 16; // FIXME? 3737 vsb.x = this.width - vsb.width; 3738 vsb.y = 0; 3739 vsb.height = this.height - 16; 3740 vsb.recomputeChildLayout(); 3741 3742 container.x = 0; 3743 container.y = 0; 3744 container.width = this.width - vsb.width; 3745 container.height = this.height - hsb.height; 3746 container.recomputeChildLayout(); 3747 } 3748 3749 HorizontalScrollbar hsb; 3750 VerticalScrollbar vsb; 3751 Widget container; 3752 } 3753 3754 /++ 3755 Bypasses automatic layout for its children, using manual positioning and sizing only. 3756 While you need to manually position them, you must ensure they are inside the StaticLayout's 3757 bounding box to avoid undefined behavior. 3758 3759 You should almost never use this. 3760 +/ 3761 class StaticLayout : Layout { 3762 /// 3763 this(Widget parent = null) { super(parent); } 3764 override void recomputeChildLayout() { 3765 registerMovement(); 3766 foreach(child; children) 3767 child.recomputeChildLayout(); 3768 } 3769 } 3770 3771 /++ 3772 Bypasses automatic positioning when being laid out. It is your responsibility to make 3773 room for this widget in the parent layout. 3774 3775 Its children are laid out normally, unless there is exactly one, in which case it takes 3776 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 3777 can do that with `padding`). 3778 +/ 3779 class StaticPosition : Layout { 3780 /// 3781 this(Widget parent = null) { super(parent); } 3782 3783 override void recomputeChildLayout() { 3784 registerMovement(); 3785 if(this.children.length == 1) { 3786 auto child = children[0]; 3787 child.x = 0; 3788 child.y = 0; 3789 child.width = this.width; 3790 child.height = this.height; 3791 child.recomputeChildLayout(); 3792 } else 3793 foreach(child; children) 3794 child.recomputeChildLayout(); 3795 } 3796 3797 } 3798 3799 /++ 3800 FixedPosition is like [StaticPosition], but its coordinates 3801 are always relative to the viewport, meaning they do not scroll with 3802 the parent content. 3803 +/ 3804 class FixedPosition : StaticPosition { 3805 /// 3806 this(Widget parent) { super(parent); } 3807 } 3808 3809 3810 /// 3811 class Window : Widget { 3812 int mouseCaptureCount = 0; 3813 Widget mouseCapturedBy; 3814 void captureMouse(Widget byWhom) { 3815 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 3816 mouseCaptureCount++; 3817 mouseCapturedBy = byWhom; 3818 win.grabInput(); 3819 } 3820 void releaseMouseCapture() { 3821 mouseCaptureCount--; 3822 mouseCapturedBy = null; 3823 win.releaseInputGrab(); 3824 } 3825 3826 /// 3827 @scriptable 3828 @property bool focused() { 3829 return win.focused; 3830 } 3831 3832 override Color backgroundColor() { 3833 version(custom_widgets) 3834 return windowBackgroundColor; 3835 else version(win32_widgets) 3836 return Color.transparent; 3837 else static assert(0); 3838 } 3839 3840 /// 3841 static int lineHeight; 3842 3843 Widget focusedWidget; 3844 3845 SimpleWindow win; 3846 3847 /// 3848 this(Widget p) { 3849 tabStop = false; 3850 super(p); 3851 } 3852 3853 private bool skipNextChar = false; 3854 3855 /// 3856 this(SimpleWindow win) { 3857 3858 static if(UsingSimpledisplayX11) { 3859 win.discardAdditionalConnectionState = &discardXConnectionState; 3860 win.recreateAdditionalConnectionState = &recreateXConnectionState; 3861 } 3862 3863 tabStop = false; 3864 super(null); 3865 this.win = win; 3866 3867 win.addEventListener((Widget.RedrawEvent) { 3868 //import std.stdio; writeln("redrawing"); 3869 this.actualRedraw(); 3870 }); 3871 3872 this.width = win.width; 3873 this.height = win.height; 3874 this.parentWindow = this; 3875 3876 win.windowResized = (int w, int h) { 3877 this.width = w; 3878 this.height = h; 3879 recomputeChildLayout(); 3880 version(win32_widgets) 3881 InvalidateRect(hwnd, null, true); 3882 redraw(); 3883 }; 3884 3885 win.onFocusChange = (bool getting) { 3886 if(this.focusedWidget) { 3887 auto evt = new Event(getting ? "focus" : "blur", this.focusedWidget); 3888 evt.dispatch(); 3889 } 3890 auto evt = new Event(getting ? "focus" : "blur", this); 3891 evt.dispatch(); 3892 }; 3893 3894 win.setEventHandlers( 3895 (MouseEvent e) { 3896 dispatchMouseEvent(e); 3897 }, 3898 (KeyEvent e) { 3899 //import std.stdio; 3900 //writefln("%x %s", cast(uint) e.key, e.key); 3901 dispatchKeyEvent(e); 3902 }, 3903 (dchar e) { 3904 if(e == 13) e = 10; // hack? 3905 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 3906 dispatchCharEvent(e); 3907 }, 3908 ); 3909 3910 addEventListener("char", (Widget, Event ev) { 3911 if(skipNextChar) { 3912 ev.preventDefault(); 3913 skipNextChar = false; 3914 } 3915 }); 3916 3917 version(win32_widgets) 3918 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 3919 3920 if(hwnd !is this.win.impl.hwnd) 3921 return 1; // we don't care... 3922 switch(msg) { 3923 3924 case WM_VSCROLL, WM_HSCROLL: 3925 auto pos = HIWORD(wParam); 3926 auto m = LOWORD(wParam); 3927 3928 auto scrollbarHwnd = cast(HWND) lParam; 3929 3930 3931 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3932 3933 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3934 3935 switch(m) { 3936 /+ 3937 // I don't think those messages are ever actually sent normally by the widget itself, 3938 // they are more used for the keyboard interface. methinks. 3939 case SB_BOTTOM: 3940 import std.stdio; writeln("end"); 3941 auto event = new Event("scrolltoend", *widgetp); 3942 event.dispatch(); 3943 //if(!event.defaultPrevented) 3944 break; 3945 case SB_TOP: 3946 import std.stdio; writeln("top"); 3947 auto event = new Event("scrolltobeginning", *widgetp); 3948 event.dispatch(); 3949 break; 3950 case SB_ENDSCROLL: 3951 // idk 3952 break; 3953 +/ 3954 case SB_LINEDOWN: 3955 auto event = new Event("scrolltonextline", *widgetp); 3956 event.dispatch(); 3957 break; 3958 case SB_LINEUP: 3959 auto event = new Event("scrolltopreviousline", *widgetp); 3960 event.dispatch(); 3961 break; 3962 case SB_PAGEDOWN: 3963 auto event = new Event("scrolltonextpage", *widgetp); 3964 event.dispatch(); 3965 break; 3966 case SB_PAGEUP: 3967 auto event = new Event("scrolltopreviouspage", *widgetp); 3968 event.dispatch(); 3969 break; 3970 case SB_THUMBPOSITION: 3971 auto event = new Event("scrolltoposition", *widgetp); 3972 event.intValue = pos; 3973 event.dispatch(); 3974 break; 3975 case SB_THUMBTRACK: 3976 // eh kinda lying but i like the real time update display 3977 auto event = new Event("scrolltoposition", *widgetp); 3978 event.intValue = pos; 3979 event.dispatch(); 3980 // the event loop doesn't seem to carry on with a requested redraw.. 3981 // so we request it to get our dirty bit set... 3982 // then we need to immediately actually redraw it too for instant feedback to user 3983 if(redrawRequested) 3984 actualRedraw(); 3985 break; 3986 default: 3987 } 3988 } else { 3989 return 1; 3990 } 3991 break; 3992 3993 case WM_CONTEXTMENU: 3994 auto hwndFrom = cast(HWND) wParam; 3995 3996 auto xPos = cast(short) LOWORD(lParam); 3997 auto yPos = cast(short) HIWORD(lParam); 3998 3999 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4000 POINT p; 4001 p.x = xPos; 4002 p.y = yPos; 4003 ScreenToClient(hwnd, &p); 4004 auto clientX = cast(ushort) p.x; 4005 auto clientY = cast(ushort) p.y; 4006 4007 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 4008 4009 if(!wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) 4010 return 1; // it didn't show above, pass message on 4011 } 4012 break; 4013 4014 case WM_NOTIFY: 4015 auto hdr = cast(NMHDR*) lParam; 4016 auto hwndFrom = hdr.hwndFrom; 4017 auto code = hdr.code; 4018 4019 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4020 return (*widgetp).handleWmNotify(hdr, code); 4021 } 4022 break; 4023 case WM_COMMAND: 4024 switch(HIWORD(wParam)) { 4025 case 0: 4026 // case BN_CLICKED: aka 0 4027 case 1: 4028 auto idm = LOWORD(wParam); 4029 if(auto item = idm in Action.mapping) { 4030 foreach(handler; (*item).triggered) 4031 handler(); 4032 /* 4033 auto event = new Event("triggered", *item); 4034 event.button = idm; 4035 event.dispatch(); 4036 */ 4037 } else { 4038 auto handle = cast(HWND) lParam; 4039 if(auto widgetp = handle in Widget.nativeMapping) { 4040 (*widgetp).handleWmCommand(HIWORD(wParam), LOWORD(wParam)); 4041 } 4042 } 4043 break; 4044 default: 4045 return 1; 4046 } 4047 break; 4048 default: return 1; // not handled, pass it on 4049 } 4050 return 0; 4051 }; 4052 4053 4054 4055 if(lineHeight == 0) { 4056 auto painter = win.draw(); 4057 lineHeight = painter.fontHeight() * 5 / 4; 4058 } 4059 } 4060 4061 version(win32_widgets) 4062 override void paint(WidgetPainter painter) { 4063 /* 4064 RECT rect; 4065 rect.right = this.width; 4066 rect.bottom = this.height; 4067 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 4068 */ 4069 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 4070 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 4071 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 4072 // since the pen is null, to fill the whole space, we need the +1 on both. 4073 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 4074 SelectObject(painter.impl.hdc, p); 4075 SelectObject(painter.impl.hdc, b); 4076 } 4077 version(custom_widgets) 4078 override void paint(WidgetPainter painter) { 4079 painter.fillColor = windowBackgroundColor; 4080 painter.outlineColor = windowBackgroundColor; 4081 painter.drawRectangle(Point(0, 0), this.width, this.height); 4082 } 4083 4084 4085 override void defaultEventHandler_keydown(Event event) { 4086 Widget _this = event.target; 4087 4088 if(event.key == Key.Tab) { 4089 /* Window tab ordering is a recursive thingy with each group */ 4090 4091 // FIXME inefficient 4092 Widget[] helper(Widget p) { 4093 if(p.hidden) 4094 return null; 4095 Widget[] childOrdering; 4096 4097 auto children = p.children.dup; 4098 4099 while(true) { 4100 // UIs should be generally small, so gonna brute force it a little 4101 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 4102 4103 Widget smallestTab; 4104 foreach(ref c; children) { 4105 if(c is null) continue; 4106 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 4107 smallestTab = c; 4108 c = null; 4109 } 4110 } 4111 if(smallestTab !is null) { 4112 if(smallestTab.tabStop && !smallestTab.hidden) 4113 childOrdering ~= smallestTab; 4114 if(!smallestTab.hidden) 4115 childOrdering ~= helper(smallestTab); 4116 } else 4117 break; 4118 4119 } 4120 4121 return childOrdering; 4122 } 4123 4124 Widget[] tabOrdering = helper(this); 4125 4126 Widget recipient; 4127 4128 if(tabOrdering.length) { 4129 bool seenThis = false; 4130 Widget previous; 4131 foreach(idx, child; tabOrdering) { 4132 if(child is focusedWidget) { 4133 4134 if(event.shiftKey) { 4135 if(idx == 0) 4136 recipient = tabOrdering[$-1]; 4137 else 4138 recipient = tabOrdering[idx - 1]; 4139 break; 4140 } 4141 4142 seenThis = true; 4143 if(idx + 1 == tabOrdering.length) { 4144 // we're at the end, either move to the next group 4145 // or start back over 4146 recipient = tabOrdering[0]; 4147 } 4148 continue; 4149 } 4150 if(seenThis) { 4151 recipient = child; 4152 break; 4153 } 4154 previous = child; 4155 } 4156 } 4157 4158 if(recipient !is null) { 4159 // import std.stdio; writeln(typeid(recipient)); 4160 recipient.focus(); 4161 /* 4162 version(win32_widgets) { 4163 if(recipient.hwnd !is null) 4164 SetFocus(recipient.hwnd); 4165 } else version(custom_widgets) { 4166 focusedWidget = recipient; 4167 } else static assert(false); 4168 */ 4169 4170 skipNextChar = true; 4171 } 4172 } 4173 4174 debug if(event.key == Key.F12) { 4175 if(devTools) { 4176 devTools.close(); 4177 devTools = null; 4178 } else { 4179 devTools = new DevToolWindow(this); 4180 devTools.show(); 4181 } 4182 } 4183 } 4184 4185 debug DevToolWindow devTools; 4186 4187 4188 /// 4189 this(int width = 500, int height = 500, string title = null) { 4190 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow); 4191 this(win); 4192 } 4193 4194 /// 4195 this(string title) { 4196 this(500, 500, title); 4197 } 4198 4199 /// 4200 @scriptable 4201 void close() { 4202 win.close(); 4203 } 4204 4205 bool dispatchKeyEvent(KeyEvent ev) { 4206 auto wid = focusedWidget; 4207 if(wid is null) 4208 wid = this; 4209 auto event = new Event(ev.pressed ? "keydown" : "keyup", wid); 4210 event.originalKeyEvent = ev; 4211 event.character = ev.character; 4212 event.key = ev.key; 4213 event.state = ev.modifierState; 4214 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 4215 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 4216 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 4217 event.dispatch(); 4218 4219 return true; 4220 } 4221 4222 bool dispatchCharEvent(dchar ch) { 4223 if(focusedWidget) { 4224 auto event = new Event("char", focusedWidget); 4225 event.character = ch; 4226 event.dispatch(); 4227 } 4228 return true; 4229 } 4230 4231 Widget mouseLastOver; 4232 Widget mouseLastDownOn; 4233 bool dispatchMouseEvent(MouseEvent ev) { 4234 auto eleR = widgetAtPoint(this, ev.x, ev.y); 4235 auto ele = eleR.widget; 4236 4237 if(mouseCapturedBy !is null) { 4238 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 4239 ele = mouseCapturedBy; 4240 } 4241 4242 // a hack to get it relative to the widget. 4243 eleR.x = ev.x; 4244 eleR.y = ev.y; 4245 auto pain = ele; 4246 while(pain) { 4247 eleR.x -= pain.x; 4248 eleR.y -= pain.y; 4249 pain = pain.parent; 4250 } 4251 4252 if(ev.type == 1) { 4253 mouseLastDownOn = ele; 4254 auto event = new Event("mousedown", ele); 4255 event.button = ev.button; 4256 event.buttonLinear = ev.buttonLinear; 4257 event.state = ev.modifierState; 4258 event.clientX = eleR.x; 4259 event.clientY = eleR.y; 4260 event.dispatch(); 4261 } else if(ev.type == 2) { 4262 auto event = new Event("mouseup", ele); 4263 event.button = ev.button; 4264 event.buttonLinear = ev.buttonLinear; 4265 event.clientX = eleR.x; 4266 event.clientY = eleR.y; 4267 event.state = ev.modifierState; 4268 event.dispatch(); 4269 if(mouseLastDownOn is ele) { 4270 event = new Event("click", ele); 4271 event.clientX = eleR.x; 4272 event.clientY = eleR.y; 4273 event.state = ev.modifierState; 4274 event.button = ev.button; 4275 event.buttonLinear = ev.buttonLinear; 4276 event.dispatch(); 4277 } 4278 } else if(ev.type == 0) { 4279 // motion 4280 Event event = new Event("mousemove", ele); 4281 event.state = ev.modifierState; 4282 event.clientX = eleR.x; 4283 event.clientY = eleR.y; 4284 event.dispatch(); 4285 4286 if(mouseLastOver !is ele) { 4287 if(ele !is null) { 4288 if(!isAParentOf(ele, mouseLastOver)) { 4289 event = new Event("mouseenter", ele); 4290 event.relatedTarget = mouseLastOver; 4291 event.sendDirectly(); 4292 4293 ele.parentWindow.win.cursor = ele.cursor; 4294 } 4295 } 4296 4297 if(mouseLastOver !is null) { 4298 if(!isAParentOf(mouseLastOver, ele)) { 4299 event = new Event("mouseleave", mouseLastOver); 4300 event.relatedTarget = ele; 4301 event.sendDirectly(); 4302 } 4303 } 4304 4305 if(ele !is null) { 4306 event = new Event("mouseover", ele); 4307 event.relatedTarget = mouseLastOver; 4308 event.dispatch(); 4309 } 4310 4311 if(mouseLastOver !is null) { 4312 event = new Event("mouseout", mouseLastOver); 4313 event.relatedTarget = ele; 4314 event.dispatch(); 4315 } 4316 4317 mouseLastOver = ele; 4318 } 4319 } 4320 4321 return true; 4322 } 4323 4324 /// Shows the window and runs the application event loop. 4325 @scriptable 4326 void loop() { 4327 show(); 4328 win.eventLoop(0); 4329 } 4330 4331 private bool firstShow = true; 4332 4333 @scriptable 4334 override void show() { 4335 bool rd = false; 4336 if(firstShow) { 4337 firstShow = false; 4338 recomputeChildLayout(); 4339 focusedWidget = getFirstFocusable(this); // FIXME: autofocus? 4340 redraw(); 4341 } 4342 win.show(); 4343 super.show(); 4344 } 4345 @scriptable 4346 override void hide() { 4347 win.hide(); 4348 super.hide(); 4349 } 4350 4351 static Widget getFirstFocusable(Widget start) { 4352 if(start.tabStop && !start.hidden) 4353 return start; 4354 4355 if(!start.hidden) 4356 foreach(child; start.children) { 4357 auto f = getFirstFocusable(child); 4358 if(f !is null) 4359 return f; 4360 } 4361 return null; 4362 } 4363 } 4364 4365 debug private class DevToolWindow : Window { 4366 Window p; 4367 4368 TextEdit parentList; 4369 TextEdit logWindow; 4370 TextLabel clickX, clickY; 4371 4372 this(Window p) { 4373 this.p = p; 4374 super(400, 300, "Developer Toolbox"); 4375 4376 logWindow = new TextEdit(this); 4377 parentList = new TextEdit(this); 4378 4379 auto hl = new HorizontalLayout(this); 4380 clickX = new TextLabel("", hl); 4381 clickY = new TextLabel("", hl); 4382 4383 parentListeners ~= p.addEventListener(EventType.click, (Event ev) { 4384 auto s = ev.srcElement; 4385 string list = s.toString(); 4386 s = s.parent; 4387 while(s) { 4388 list ~= "\n"; 4389 list ~= s.toString(); 4390 s = s.parent; 4391 } 4392 parentList.content = list; 4393 4394 clickX.label = toInternal!string(ev.clientX); 4395 clickY.label = toInternal!string(ev.clientY); 4396 }); 4397 } 4398 4399 EventListener[] parentListeners; 4400 4401 override void close() { 4402 assert(p !is null); 4403 foreach(p; parentListeners) 4404 p.disconnect(); 4405 parentListeners = null; 4406 p.devTools = null; 4407 p = null; 4408 super.close(); 4409 } 4410 4411 override void defaultEventHandler_keydown(Event ev) { 4412 if(ev.key == Key.F12) { 4413 this.close(); 4414 p.devTools = null; 4415 } else { 4416 super.defaultEventHandler_keydown(ev); 4417 } 4418 } 4419 4420 void log(T...)(T t) { 4421 string str; 4422 import std.conv; 4423 foreach(i; t) 4424 str ~= to!string(i); 4425 str ~= "\n"; 4426 logWindow.addText(str); 4427 4428 logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 4429 } 4430 } 4431 4432 /++ 4433 A dialog is a transient window that intends to get information from 4434 the user before being dismissed. 4435 +/ 4436 abstract class Dialog : Window { 4437 /// 4438 this(int width, int height, string title = null) { 4439 super(width, height, title); 4440 } 4441 4442 /// 4443 abstract void OK(); 4444 4445 /// 4446 void Cancel() { 4447 this.close(); 4448 } 4449 } 4450 4451 /// 4452 class LabeledLineEdit : Widget { 4453 /// 4454 this(string label, Widget parent = null) { 4455 super(parent); 4456 tabStop = false; 4457 auto hl = new HorizontalLayout(this); 4458 this.label = new TextLabel(label, hl); 4459 this.lineEdit = new LineEdit(hl); 4460 } 4461 TextLabel label; /// 4462 LineEdit lineEdit; /// 4463 4464 override int minHeight() { return Window.lineHeight + 4; } 4465 override int maxHeight() { return Window.lineHeight + 4; } 4466 4467 /// 4468 string content() { 4469 return lineEdit.content; 4470 } 4471 /// 4472 void content(string c) { 4473 return lineEdit.content(c); 4474 } 4475 4476 /// 4477 void selectAll() { 4478 lineEdit.selectAll(); 4479 } 4480 4481 override void focus() { 4482 lineEdit.focus(); 4483 } 4484 } 4485 4486 /// 4487 class MainWindow : Window { 4488 /// 4489 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 4490 super(initialWidth, initialHeight, title); 4491 4492 _clientArea = new ClientAreaWidget(); 4493 _clientArea.x = 0; 4494 _clientArea.y = 0; 4495 _clientArea.width = this.width; 4496 _clientArea.height = this.height; 4497 _clientArea.tabStop = false; 4498 4499 super.addChild(_clientArea); 4500 4501 statusBar = new StatusBar(this); 4502 } 4503 4504 /++ 4505 Adds a menu and toolbar from annotated functions. 4506 4507 --- 4508 struct Commands { 4509 @menu("File") { 4510 void New() {} 4511 void Open() {} 4512 void Save() {} 4513 @separator 4514 void Exit() @accelerator("Alt+F4") { 4515 window.close(); 4516 } 4517 } 4518 4519 @menu("Edit") { 4520 void Undo() { 4521 undo(); 4522 } 4523 @separator 4524 void Cut() {} 4525 void Copy() {} 4526 void Paste() {} 4527 } 4528 4529 @menu("Help") { 4530 void About() {} 4531 } 4532 } 4533 4534 Commands commands; 4535 4536 window.setMenuAndToolbarFromAnnotatedCode(commands); 4537 --- 4538 4539 +/ 4540 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 4541 setMenuAndToolbarFromAnnotatedCode_internal(t); 4542 } 4543 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 4544 setMenuAndToolbarFromAnnotatedCode_internal(t); 4545 } 4546 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 4547 Action[] toolbarActions; 4548 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 4549 Menu[string] mcs; 4550 4551 foreach(menu; menuBar.subMenus) { 4552 mcs[menu.label] = menu; 4553 } 4554 4555 void delegate() triggering; 4556 4557 foreach(memberName; __traits(derivedMembers, T)) { 4558 static if(__traits(compiles, triggering = &__traits(getMember, t, memberName))) { 4559 .menu menu; 4560 .toolbar toolbar; 4561 bool separator; 4562 .accelerator accelerator; 4563 .hotkey hotkey; 4564 .icon icon; 4565 string label; 4566 string tip; 4567 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 4568 static if(is(typeof(attr) == .menu)) 4569 menu = attr; 4570 else static if(is(typeof(attr) == .toolbar)) 4571 toolbar = attr; 4572 else static if(is(attr == .separator)) 4573 separator = true; 4574 else static if(is(typeof(attr) == .accelerator)) 4575 accelerator = attr; 4576 else static if(is(typeof(attr) == .hotkey)) 4577 hotkey = attr; 4578 else static if(is(typeof(attr) == .icon)) 4579 icon = attr; 4580 else static if(is(typeof(attr) == .label)) 4581 label = attr.label; 4582 else static if(is(typeof(attr) == .tip)) 4583 tip = attr.tip; 4584 } 4585 4586 if(menu !is .menu.init || toolbar !is .toolbar.init) { 4587 ushort correctIcon = icon.id; // FIXME 4588 if(label.length == 0) 4589 label = memberName; 4590 auto action = new Action(label, correctIcon, &__traits(getMember, t, memberName)); 4591 4592 if(accelerator.keyString.length) { 4593 auto ke = KeyEvent.parse(accelerator.keyString); 4594 action.accelerator = ke; 4595 accelerators[ke.toStr] = &__traits(getMember, t, memberName); 4596 } 4597 4598 if(toolbar !is .toolbar.init) 4599 toolbarActions ~= action; 4600 if(menu !is .menu.init) { 4601 Menu mc; 4602 if(menu.name in mcs) { 4603 mc = mcs[menu.name]; 4604 } else { 4605 mc = new Menu(menu.name); 4606 menuBar.addItem(mc); 4607 mcs[menu.name] = mc; 4608 } 4609 4610 if(separator) 4611 mc.addSeparator(); 4612 mc.addItem(new MenuItem(action)); 4613 } 4614 } 4615 } 4616 } 4617 4618 this.menuBar = menuBar; 4619 4620 if(toolbarActions.length) { 4621 auto tb = new ToolBar(toolbarActions, this); 4622 } 4623 } 4624 4625 void delegate()[string] accelerators; 4626 4627 override void defaultEventHandler_keydown(Event event) { 4628 auto str = event.originalKeyEvent.toStr; 4629 if(auto acl = str in accelerators) 4630 (*acl)(); 4631 super.defaultEventHandler_keydown(event); 4632 } 4633 4634 override void defaultEventHandler_mouseover(Event event) { 4635 super.defaultEventHandler_mouseover(event); 4636 if(this.statusBar !is null && event.target.statusTip.length) 4637 this.statusBar.parts[0].content = event.target.statusTip; 4638 else if(this.statusBar !is null && this.statusTip.length) 4639 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 4640 } 4641 4642 override void addChild(Widget c, int position = int.max) { 4643 if(auto tb = cast(ToolBar) c) 4644 version(win32_widgets) 4645 super.addChild(c, 0); 4646 else version(custom_widgets) 4647 super.addChild(c, menuBar ? 1 : 0); 4648 else static assert(0); 4649 else 4650 clientArea.addChild(c, position); 4651 } 4652 4653 ToolBar _toolBar; 4654 /// 4655 ToolBar toolBar() { return _toolBar; } 4656 /// 4657 ToolBar toolBar(ToolBar t) { 4658 _toolBar = t; 4659 foreach(child; this.children) 4660 if(child is t) 4661 return t; 4662 version(win32_widgets) 4663 super.addChild(t, 0); 4664 else version(custom_widgets) 4665 super.addChild(t, menuBar ? 1 : 0); 4666 else static assert(0); 4667 return t; 4668 } 4669 4670 MenuBar _menu; 4671 /// 4672 MenuBar menuBar() { return _menu; } 4673 /// 4674 MenuBar menuBar(MenuBar m) { 4675 if(m is _menu) { 4676 version(custom_widgets) 4677 recomputeChildLayout(); 4678 return m; 4679 } 4680 4681 if(_menu !is null) { 4682 // make sure it is sanely removed 4683 // FIXME 4684 } 4685 4686 _menu = m; 4687 4688 version(win32_widgets) { 4689 SetMenu(parentWindow.win.impl.hwnd, m.handle); 4690 } else version(custom_widgets) { 4691 super.addChild(m, 0); 4692 4693 // clientArea.y = menu.height; 4694 // clientArea.height = this.height - menu.height; 4695 4696 recomputeChildLayout(); 4697 } else static assert(false); 4698 4699 return _menu; 4700 } 4701 private Widget _clientArea; 4702 /// 4703 @property Widget clientArea() { return _clientArea; } 4704 protected @property void clientArea(Widget wid) { 4705 _clientArea = wid; 4706 } 4707 4708 private StatusBar _statusBar; 4709 /// 4710 @property StatusBar statusBar() { return _statusBar; } 4711 /// 4712 @property void statusBar(StatusBar bar) { 4713 _statusBar = bar; 4714 super.addChild(_statusBar); 4715 } 4716 4717 /// 4718 @property string title() { return parentWindow.win.title; } 4719 /// 4720 @property void title(string title) { parentWindow.win.title = title; } 4721 } 4722 4723 class ClientAreaWidget : Widget { 4724 this(Widget parent = null) { 4725 super(parent); 4726 //sa = new ScrollableWidget(this); 4727 } 4728 /* 4729 ScrollableWidget sa; 4730 override void addChild(Widget w, int position) { 4731 if(sa is null) 4732 super.addChild(w, position); 4733 else { 4734 sa.addChild(w, position); 4735 sa.setContentSize(this.minWidth + 1, this.minHeight); 4736 import std.stdio; writeln(sa.contentWidth, "x", sa.contentHeight); 4737 } 4738 } 4739 */ 4740 } 4741 4742 /** 4743 Toolbars are lists of buttons (typically icons) that appear under the menu. 4744 Each button ought to correspond to a menu item. 4745 */ 4746 class ToolBar : Widget { 4747 version(win32_widgets) { 4748 private const int idealHeight; 4749 override int minHeight() { return idealHeight; } 4750 override int maxHeight() { return idealHeight; } 4751 } else version(custom_widgets) { 4752 override int minHeight() { return toolbarIconSize; }// Window.lineHeight * 3/2; } 4753 override int maxHeight() { return toolbarIconSize; } //Window.lineHeight * 3/2; } 4754 } else static assert(false); 4755 override int heightStretchiness() { return 0; } 4756 4757 version(win32_widgets) 4758 HIMAGELIST imageList; 4759 4760 this(Widget parent) { 4761 this(null, parent); 4762 } 4763 4764 /// 4765 this(Action[] actions, Widget parent = null) { 4766 super(parent); 4767 4768 tabStop = false; 4769 4770 version(win32_widgets) { 4771 // so i like how the flat thing looks on windows, but not on wine 4772 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 4773 // leave it commented 4774 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 4775 4776 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 4777 4778 imageList = ImageList_Create( 4779 // width, height 4780 16, 16, 4781 ILC_COLOR16 | ILC_MASK, 4782 16 /*numberOfButtons*/, 0); 4783 4784 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageList); 4785 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 4786 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 4787 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 4788 4789 TBBUTTON[] buttons; 4790 4791 // FIXME: I_IMAGENONE is if here is no icon 4792 foreach(action; actions) 4793 buttons ~= TBBUTTON(MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), action.id, TBSTATE_ENABLED, 0, 0, 0, cast(int) toWstringzInternal(action.label)); 4794 4795 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 4796 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 4797 4798 SIZE size; 4799 import core.sys.windows.commctrl; 4800 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 4801 idealHeight = size.cy + 4; // the plus 4 is a hack 4802 4803 /* 4804 RECT rect; 4805 GetWindowRect(hwnd, &rect); 4806 idealHeight = rect.bottom - rect.top + 10; // the +10 is a hack since the size right now doesn't look right on a real Windows XP box 4807 */ 4808 4809 assert(idealHeight); 4810 } else version(custom_widgets) { 4811 foreach(action; actions) 4812 addChild(new ToolButton(action)); 4813 } else static assert(false); 4814 } 4815 4816 override void recomputeChildLayout() { 4817 .recomputeChildLayout!"width"(this); 4818 } 4819 } 4820 4821 enum toolbarIconSize = 24; 4822 4823 /// 4824 class ToolButton : Button { 4825 /// 4826 this(string label, Widget parent = null) { 4827 super(label, parent); 4828 tabStop = false; 4829 } 4830 /// 4831 this(Action action, Widget parent = null) { 4832 super(action.label, parent); 4833 tabStop = false; 4834 this.action = action; 4835 } 4836 4837 version(custom_widgets) 4838 override void defaultEventHandler_click(Event event) { 4839 foreach(handler; action.triggered) 4840 handler(); 4841 } 4842 4843 Action action; 4844 4845 override int maxWidth() { return toolbarIconSize; } 4846 override int minWidth() { return toolbarIconSize; } 4847 override int maxHeight() { return toolbarIconSize; } 4848 override int minHeight() { return toolbarIconSize; } 4849 4850 version(custom_widgets) 4851 override void paint(WidgetPainter painter) { 4852 this.draw3dFrame(painter, isDepressed ? FrameStyle.sunk : FrameStyle.risen, currentButtonColor); 4853 4854 painter.outlineColor = Color.black; 4855 4856 // I want to get from 16 to 24. that's * 3 / 2 4857 static assert(toolbarIconSize >= 16); 4858 enum multiplier = toolbarIconSize / 8; 4859 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 4860 switch(action.iconId) { 4861 case GenericIcons.New: 4862 painter.fillColor = Color.white; 4863 painter.drawPolygon( 4864 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 4865 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 4866 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 4867 ); 4868 break; 4869 case GenericIcons.Save: 4870 painter.fillColor = Color.white; 4871 painter.outlineColor = Color.black; 4872 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 4873 4874 // the label 4875 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 4876 4877 // the slider 4878 painter.fillColor = Color.black; 4879 painter.outlineColor = Color.black; 4880 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 4881 4882 painter.fillColor = Color.white; 4883 painter.outlineColor = Color.white; 4884 // the disc window 4885 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 4886 break; 4887 case GenericIcons.Open: 4888 painter.fillColor = Color.white; 4889 painter.drawPolygon( 4890 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 4891 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 4892 painter.drawPolygon( 4893 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 4894 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 4895 Point(2, 6) * multiplier / divisor); 4896 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 4897 break; 4898 case GenericIcons.Copy: 4899 painter.fillColor = Color.white; 4900 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 4901 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 4902 break; 4903 case GenericIcons.Cut: 4904 painter.fillColor = Color.transparent; 4905 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 4906 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 4907 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 4908 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 4909 break; 4910 case GenericIcons.Paste: 4911 painter.fillColor = Color.white; 4912 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 4913 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 4914 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 4915 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 4916 painter.fillColor = Color.black; 4917 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 4918 break; 4919 case GenericIcons.Help: 4920 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 4921 break; 4922 case GenericIcons.Undo: 4923 painter.fillColor = Color.transparent; 4924 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 4925 painter.outlineColor = Color.black; 4926 painter.fillColor = Color.black; 4927 painter.drawPolygon( 4928 Point(4, 4) * multiplier / divisor, 4929 Point(8, 2) * multiplier / divisor, 4930 Point(8, 6) * multiplier / divisor, 4931 Point(4, 4) * multiplier / divisor, 4932 ); 4933 break; 4934 case GenericIcons.Redo: 4935 painter.fillColor = Color.transparent; 4936 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 4937 painter.outlineColor = Color.black; 4938 painter.fillColor = Color.black; 4939 painter.drawPolygon( 4940 Point(10, 4) * multiplier / divisor, 4941 Point(6, 2) * multiplier / divisor, 4942 Point(6, 6) * multiplier / divisor, 4943 Point(10, 4) * multiplier / divisor, 4944 ); 4945 break; 4946 default: 4947 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 4948 } 4949 } 4950 4951 } 4952 4953 4954 /// 4955 class MenuBar : Widget { 4956 MenuItem[] items; 4957 Menu[] subMenus; 4958 4959 version(win32_widgets) { 4960 HMENU handle; 4961 /// 4962 this(Widget parent = null) { 4963 super(parent); 4964 4965 handle = CreateMenu(); 4966 tabStop = false; 4967 } 4968 } else version(custom_widgets) { 4969 /// 4970 this(Widget parent = null) { 4971 tabStop = false; // these are selected some other way 4972 super(parent); 4973 } 4974 4975 mixin Padding!q{2}; 4976 } else static assert(false); 4977 4978 version(custom_widgets) 4979 override void paint(WidgetPainter painter) { 4980 draw3dFrame(this, painter, FrameStyle.risen); 4981 } 4982 4983 /// 4984 MenuItem addItem(MenuItem item) { 4985 this.addChild(item); 4986 items ~= item; 4987 version(win32_widgets) { 4988 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 4989 } 4990 return item; 4991 } 4992 4993 4994 /// 4995 Menu addItem(Menu item) { 4996 4997 subMenus ~= item; 4998 4999 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 5000 5001 addChild(mbItem); 5002 items ~= mbItem; 5003 5004 version(win32_widgets) { 5005 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 5006 } else version(custom_widgets) { 5007 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 5008 item.popup(mbItem); 5009 }; 5010 } else static assert(false); 5011 5012 return item; 5013 } 5014 5015 override void recomputeChildLayout() { 5016 .recomputeChildLayout!"width"(this); 5017 } 5018 5019 override int maxHeight() { return Window.lineHeight + 4; } 5020 override int minHeight() { return Window.lineHeight + 4; } 5021 } 5022 5023 5024 /** 5025 Status bars appear at the bottom of a MainWindow. 5026 They are made out of Parts, with a width and content. 5027 5028 They can have multiple parts or be in simple mode. FIXME: implement 5029 5030 5031 sb.parts[0].content = "Status bar text!"; 5032 */ 5033 class StatusBar : Widget { 5034 private Part[] partsArray; 5035 /// 5036 struct Parts { 5037 @disable this(); 5038 this(StatusBar owner) { this.owner = owner; } 5039 //@disable this(this); 5040 /// 5041 @property int length() { return cast(int) owner.partsArray.length; } 5042 private StatusBar owner; 5043 private this(StatusBar owner, Part[] parts) { 5044 this.owner.partsArray = parts; 5045 this.owner = owner; 5046 } 5047 /// 5048 Part opIndex(int p) { 5049 if(owner.partsArray.length == 0) 5050 this ~= new StatusBar.Part(300); 5051 return owner.partsArray[p]; 5052 } 5053 5054 /// 5055 Part opOpAssign(string op : "~" )(Part p) { 5056 assert(owner.partsArray.length < 255); 5057 p.owner = this.owner; 5058 p.idx = cast(int) owner.partsArray.length; 5059 owner.partsArray ~= p; 5060 version(win32_widgets) { 5061 int[256] pos; 5062 int cpos = 0; 5063 foreach(idx, part; owner.partsArray) { 5064 if(part.width) 5065 cpos += part.width; 5066 else 5067 cpos += 100; 5068 5069 if(idx + 1 == owner.partsArray.length) 5070 pos[idx] = -1; 5071 else 5072 pos[idx] = cpos; 5073 } 5074 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(int) pos.ptr); 5075 } else version(custom_widgets) { 5076 owner.redraw(); 5077 } else static assert(false); 5078 5079 return p; 5080 } 5081 } 5082 5083 private Parts _parts; 5084 /// 5085 @property Parts parts() { 5086 return _parts; 5087 } 5088 5089 /// 5090 static class Part { 5091 int width; 5092 StatusBar owner; 5093 5094 /// 5095 this(int w = 100) { width = w; } 5096 5097 private int idx; 5098 private string _content; 5099 /// 5100 @property string content() { return _content; } 5101 /// 5102 @property void content(string s) { 5103 version(win32_widgets) { 5104 _content = s; 5105 WCharzBuffer bfr = WCharzBuffer(s); 5106 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 5107 } else version(custom_widgets) { 5108 if(_content != s) { 5109 _content = s; 5110 owner.redraw(); 5111 } 5112 } else static assert(false); 5113 } 5114 } 5115 string simpleModeContent; 5116 bool inSimpleMode; 5117 5118 5119 /// 5120 this(Widget parent = null) { 5121 super(null); // FIXME 5122 _parts = Parts(this); 5123 tabStop = false; 5124 version(win32_widgets) { 5125 parentWindow = parent.parentWindow; 5126 createWin32Window(this, "msctls_statusbar32"w, "", 0); 5127 5128 RECT rect; 5129 GetWindowRect(hwnd, &rect); 5130 idealHeight = rect.bottom - rect.top; 5131 assert(idealHeight); 5132 } else version(custom_widgets) { 5133 } else static assert(false); 5134 } 5135 5136 version(custom_widgets) 5137 override void paint(WidgetPainter painter) { 5138 this.draw3dFrame(painter, FrameStyle.sunk); 5139 int cpos = 0; 5140 int remainingLength = this.width; 5141 foreach(idx, part; this.partsArray) { 5142 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 5143 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 5144 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk); 5145 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 5146 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 5147 cpos += partWidth; 5148 remainingLength -= partWidth; 5149 } 5150 } 5151 5152 5153 version(win32_widgets) { 5154 private const int idealHeight; 5155 override int maxHeight() { return idealHeight; } 5156 override int minHeight() { return idealHeight; } 5157 } else version(custom_widgets) { 5158 override int maxHeight() { return Window.lineHeight + 4; } 5159 override int minHeight() { return Window.lineHeight + 4; } 5160 } else static assert(false); 5161 } 5162 5163 /// Displays an in-progress indicator without known values 5164 version(none) 5165 class IndefiniteProgressBar : Widget { 5166 version(win32_widgets) 5167 this(Widget parent = null) { 5168 super(parent); 5169 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 5170 tabStop = false; 5171 } 5172 override int minHeight() { return 10; } 5173 } 5174 5175 /// A progress bar with a known endpoint and completion amount 5176 class ProgressBar : Widget { 5177 this(Widget parent = null) { 5178 version(win32_widgets) { 5179 super(parent); 5180 createWin32Window(this, "msctls_progress32"w, "", 0); 5181 tabStop = false; 5182 } else version(custom_widgets) { 5183 super(parent); 5184 max = 100; 5185 step = 10; 5186 tabStop = false; 5187 } else static assert(0); 5188 } 5189 5190 version(custom_widgets) 5191 override void paint(WidgetPainter painter) { 5192 this.draw3dFrame(painter, FrameStyle.sunk); 5193 painter.fillColor = progressBarColor; 5194 painter.drawRectangle(Point(0, 0), width * current / max, height); 5195 } 5196 5197 5198 version(custom_widgets) { 5199 int current; 5200 int max; 5201 int step; 5202 } 5203 5204 /// 5205 void advanceOneStep() { 5206 version(win32_widgets) 5207 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 5208 else version(custom_widgets) 5209 addToPosition(step); 5210 else static assert(false); 5211 } 5212 5213 /// 5214 void setStepIncrement(int increment) { 5215 version(win32_widgets) 5216 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 5217 else version(custom_widgets) 5218 step = increment; 5219 else static assert(false); 5220 } 5221 5222 /// 5223 void addToPosition(int amount) { 5224 version(win32_widgets) 5225 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 5226 else version(custom_widgets) 5227 setPosition(current + amount); 5228 else static assert(false); 5229 } 5230 5231 /// 5232 void setPosition(int pos) { 5233 version(win32_widgets) 5234 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 5235 else version(custom_widgets) { 5236 current = pos; 5237 if(current > max) 5238 current = max; 5239 redraw(); 5240 } 5241 else static assert(false); 5242 } 5243 5244 /// 5245 void setRange(ushort min, ushort max) { 5246 version(win32_widgets) 5247 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 5248 else version(custom_widgets) { 5249 this.max = max; 5250 } 5251 else static assert(false); 5252 } 5253 5254 override int minHeight() { return 10; } 5255 } 5256 5257 /// 5258 class Fieldset : Widget { 5259 // FIXME: on Windows,it doesn't draw the background on the label 5260 // on X, it doesn't fix the clipping rectangle for it 5261 version(win32_widgets) 5262 override int paddingTop() { return Window.lineHeight; } 5263 else version(custom_widgets) 5264 override int paddingTop() { return Window.lineHeight + 2; } 5265 else static assert(false); 5266 override int paddingBottom() { return 6; } 5267 override int paddingLeft() { return 6; } 5268 override int paddingRight() { return 6; } 5269 5270 override int marginLeft() { return 6; } 5271 override int marginRight() { return 6; } 5272 override int marginTop() { return 2; } 5273 override int marginBottom() { return 2; } 5274 5275 string legend; 5276 5277 /// 5278 this(string legend, Widget parent) { 5279 version(win32_widgets) { 5280 super(parent); 5281 this.legend = legend; 5282 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 5283 tabStop = false; 5284 } else version(custom_widgets) { 5285 super(parent); 5286 tabStop = false; 5287 this.legend = legend; 5288 } else static assert(0); 5289 } 5290 5291 version(custom_widgets) 5292 override void paint(WidgetPainter painter) { 5293 painter.fillColor = Color.transparent; 5294 painter.pen = Pen(Color.black, 1); 5295 painter.drawRectangle(Point(0, Window.lineHeight / 2), width, height - Window.lineHeight / 2); 5296 5297 auto tx = painter.textSize(legend); 5298 painter.outlineColor = Color.transparent; 5299 5300 static if(UsingSimpledisplayX11) { 5301 painter.fillColor = windowBackgroundColor; 5302 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 5303 } else version(Windows) { 5304 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5305 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 5306 SelectObject(painter.impl.hdc, b); 5307 } else static assert(0); 5308 painter.outlineColor = Color.black; 5309 painter.drawText(Point(8, 0), legend); 5310 } 5311 5312 5313 override int maxHeight() { 5314 auto m = paddingTop() + paddingBottom(); 5315 foreach(child; children) { 5316 m += child.maxHeight(); 5317 m += child.marginBottom(); 5318 m += child.marginTop(); 5319 } 5320 return m + 6; 5321 } 5322 5323 override int minHeight() { 5324 auto m = paddingTop() + paddingBottom(); 5325 foreach(child; children) { 5326 m += child.minHeight(); 5327 m += child.marginBottom(); 5328 m += child.marginTop(); 5329 } 5330 return m + 6; 5331 } 5332 } 5333 5334 /// Draws a line 5335 class HorizontalRule : Widget { 5336 mixin Margin!q{ 2 }; 5337 override int minHeight() { return 2; } 5338 override int maxHeight() { return 2; } 5339 5340 /// 5341 this(Widget parent = null) { 5342 super(parent); 5343 } 5344 5345 override void paint(WidgetPainter painter) { 5346 painter.outlineColor = darkAccentColor; 5347 painter.drawLine(Point(0, 0), Point(width, 0)); 5348 painter.outlineColor = lightAccentColor; 5349 painter.drawLine(Point(0, 1), Point(width, 1)); 5350 } 5351 } 5352 5353 /// ditto 5354 class VerticalRule : Widget { 5355 mixin Margin!q{ 2 }; 5356 override int minWidth() { return 2; } 5357 override int maxWidth() { return 2; } 5358 5359 /// 5360 this(Widget parent = null) { 5361 super(parent); 5362 } 5363 5364 override void paint(WidgetPainter painter) { 5365 painter.outlineColor = darkAccentColor; 5366 painter.drawLine(Point(0, 0), Point(0, height)); 5367 painter.outlineColor = lightAccentColor; 5368 painter.drawLine(Point(1, 0), Point(1, height)); 5369 } 5370 } 5371 5372 5373 /// 5374 class Menu : Window { 5375 void remove() { 5376 foreach(i, child; parentWindow.children) 5377 if(child is this) { 5378 parentWindow.children = parentWindow.children[0 .. i] ~ parentWindow.children[i + 1 .. $]; 5379 break; 5380 } 5381 parentWindow.redraw(); 5382 5383 parentWindow.releaseMouseCapture(); 5384 } 5385 5386 /// 5387 void addSeparator() { 5388 version(win32_widgets) 5389 AppendMenu(handle, MF_SEPARATOR, 0, null); 5390 else version(custom_widgets) 5391 auto hr = new HorizontalRule(this); 5392 else static assert(0); 5393 } 5394 5395 override int paddingTop() { return 4; } 5396 override int paddingBottom() { return 4; } 5397 override int paddingLeft() { return 2; } 5398 override int paddingRight() { return 2; } 5399 5400 version(win32_widgets) {} 5401 else version(custom_widgets) { 5402 SimpleWindow dropDown; 5403 Widget menuParent; 5404 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 5405 this.menuParent = parent; 5406 5407 int w = 150; 5408 int h = paddingTop + paddingBottom; 5409 if(this.children.length) { 5410 // hacking it to get the ideal height out of recomputeChildLayout 5411 this.width = w; 5412 this.height = h; 5413 this.recomputeChildLayout(); 5414 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 5415 h += paddingBottom; 5416 5417 h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances 5418 } 5419 5420 if(offsetY == int.min) 5421 offsetY = parent.parentWindow.lineHeight; 5422 5423 auto coord = parent.globalCoordinates(); 5424 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 5425 this.x = 0; 5426 this.y = 0; 5427 this.width = dropDown.width; 5428 this.height = dropDown.height; 5429 this.drawableWindow = dropDown; 5430 this.recomputeChildLayout(); 5431 5432 static if(UsingSimpledisplayX11) 5433 XSync(XDisplayConnection.get, 0); 5434 5435 dropDown.visibilityChanged = (bool visible) { 5436 if(visible) { 5437 this.redraw(); 5438 dropDown.grabInput(); 5439 } else { 5440 dropDown.releaseInputGrab(); 5441 } 5442 }; 5443 5444 dropDown.show(); 5445 5446 bool firstClick = true; 5447 5448 clickListener = this.addEventListener(EventType.click, (Event ev) { 5449 if(firstClick) { 5450 firstClick = false; 5451 //return; 5452 } 5453 //if(ev.clientX < 0 || ev.clientY < 0 || ev.clientX > width || ev.clientY > height) 5454 unpopup(); 5455 }); 5456 } 5457 5458 EventListener clickListener; 5459 } 5460 else static assert(false); 5461 5462 version(custom_widgets) 5463 void unpopup() { 5464 mouseLastOver = mouseLastDownOn = null; 5465 dropDown.hide(); 5466 if(!menuParent.parentWindow.win.closed) { 5467 if(auto maw = cast(MouseActivatedWidget) menuParent) { 5468 maw.isDepressed = false; 5469 maw.isHovering = false; 5470 maw.redraw(); 5471 } 5472 menuParent.parentWindow.win.focus(); 5473 } 5474 clickListener.disconnect(); 5475 } 5476 5477 MenuItem[] items; 5478 5479 /// 5480 MenuItem addItem(MenuItem item) { 5481 addChild(item); 5482 items ~= item; 5483 version(win32_widgets) { 5484 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 5485 } 5486 return item; 5487 } 5488 5489 string label; 5490 5491 version(win32_widgets) { 5492 HMENU handle; 5493 /// 5494 this(string label, Widget parent = null) { 5495 super(parent); 5496 this.label = label; 5497 handle = CreatePopupMenu(); 5498 } 5499 } else version(custom_widgets) { 5500 /// 5501 this(string label, Widget parent = null) { 5502 5503 if(dropDown) { 5504 dropDown.close(); 5505 } 5506 dropDown = new SimpleWindow( 5507 150, 4, 5508 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow/*, window*/); 5509 5510 this.label = label; 5511 5512 super(dropDown); 5513 } 5514 } else static assert(false); 5515 5516 override int maxHeight() { return Window.lineHeight; } 5517 override int minHeight() { return Window.lineHeight; } 5518 5519 version(custom_widgets) 5520 override void paint(WidgetPainter painter) { 5521 this.draw3dFrame(painter, FrameStyle.risen); 5522 } 5523 } 5524 5525 /// 5526 class MenuItem : MouseActivatedWidget { 5527 Menu submenu; 5528 5529 Action action; 5530 string label; 5531 5532 override int paddingLeft() { return 4; } 5533 5534 override int maxHeight() { return Window.lineHeight + 4; } 5535 override int minHeight() { return Window.lineHeight + 4; } 5536 override int minWidth() { return Window.lineHeight * cast(int) label.length + 8; } 5537 override int maxWidth() { 5538 if(cast(MenuBar) parent) { 5539 return Window.lineHeight / 2 * cast(int) label.length + 8; 5540 } 5541 return int.max; 5542 } 5543 /// 5544 this(string lbl, Widget parent = null) { 5545 super(parent); 5546 //label = lbl; // FIXME 5547 foreach(char ch; lbl) // FIXME 5548 if(ch != '&') // FIXME 5549 label ~= ch; // FIXME 5550 tabStop = false; // these are selected some other way 5551 } 5552 5553 version(custom_widgets) 5554 override void paint(WidgetPainter painter) { 5555 if(isDepressed) 5556 this.draw3dFrame(painter, FrameStyle.sunk); 5557 if(isHovering) 5558 painter.outlineColor = activeMenuItemColor; 5559 else 5560 painter.outlineColor = Color.black; 5561 painter.fillColor = Color.transparent; 5562 painter.drawText(Point(cast(MenuBar) this.parent ? 4 : 20, 2), label, Point(width, height), TextAlignment.Left); 5563 if(action && action.accelerator !is KeyEvent.init) { 5564 painter.drawText(Point(cast(MenuBar) this.parent ? 4 : 20, 2), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right); 5565 5566 } 5567 } 5568 5569 5570 /// 5571 this(Action action, Widget parent = null) { 5572 assert(action !is null); 5573 this(action.label); 5574 this.action = action; 5575 tabStop = false; // these are selected some other way 5576 } 5577 5578 override void defaultEventHandler_triggered(Event event) { 5579 if(action) 5580 foreach(handler; action.triggered) 5581 handler(); 5582 5583 if(auto pmenu = cast(Menu) this.parent) 5584 pmenu.remove(); 5585 5586 super.defaultEventHandler_triggered(event); 5587 } 5588 } 5589 5590 version(win32_widgets) 5591 /// 5592 class MouseActivatedWidget : Widget { 5593 bool isChecked() { 5594 assert(hwnd); 5595 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 5596 5597 } 5598 void isChecked(bool state) { 5599 assert(hwnd); 5600 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 5601 5602 } 5603 5604 override void handleWmCommand(ushort cmd, ushort id) { 5605 auto event = new Event("triggered", this); 5606 event.dispatch(); 5607 } 5608 5609 this(Widget parent = null) { 5610 super(parent); 5611 } 5612 } 5613 else version(custom_widgets) 5614 /// 5615 class MouseActivatedWidget : Widget { 5616 bool isDepressed = false; 5617 bool isHovering = false; 5618 bool isChecked = false; 5619 5620 override void attachedToWindow(Window w) { 5621 w.addEventListener("mouseup", delegate (Widget _this, Event ev) { 5622 isDepressed = false; 5623 }); 5624 } 5625 5626 this(Widget parent = null) { 5627 super(parent); 5628 addEventListener("mouseenter", delegate (Widget _this, Event ev) { 5629 isHovering = true; 5630 redraw(); 5631 }); 5632 5633 addEventListener("mouseleave", delegate (Widget _this, Event ev) { 5634 isHovering = false; 5635 isDepressed = false; 5636 redraw(); 5637 }); 5638 5639 addEventListener("mousedown", delegate (Widget _this, Event ev) { 5640 isDepressed = true; 5641 redraw(); 5642 }); 5643 5644 addEventListener("mouseup", delegate (Widget _this, Event ev) { 5645 isDepressed = false; 5646 redraw(); 5647 }); 5648 } 5649 5650 override void defaultEventHandler_focus(Event ev) { 5651 super.defaultEventHandler_focus(ev); 5652 this.redraw(); 5653 } 5654 override void defaultEventHandler_blur(Event ev) { 5655 super.defaultEventHandler_blur(ev); 5656 isDepressed = false; 5657 isHovering = false; 5658 this.redraw(); 5659 } 5660 override void defaultEventHandler_keydown(Event ev) { 5661 super.defaultEventHandler_keydown(ev); 5662 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 5663 isDepressed = true; 5664 this.redraw(); 5665 } 5666 } 5667 override void defaultEventHandler_keyup(Event ev) { 5668 super.defaultEventHandler_keyup(ev); 5669 if(!isDepressed) 5670 return; 5671 isDepressed = false; 5672 this.redraw(); 5673 5674 auto event = new Event("triggered", this); 5675 event.sendDirectly(); 5676 } 5677 override void defaultEventHandler_click(Event ev) { 5678 super.defaultEventHandler_click(ev); 5679 if(this.tabStop) 5680 this.focus(); 5681 auto event = new Event("triggered", this); 5682 event.sendDirectly(); 5683 } 5684 5685 } 5686 else static assert(false); 5687 5688 /* 5689 /++ 5690 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 5691 5692 Basically the same as a checkbox. 5693 +/ 5694 class OnOffSwitch : MouseActivatedWidget { 5695 5696 } 5697 */ 5698 5699 /// 5700 class Checkbox : MouseActivatedWidget { 5701 5702 version(win32_widgets) { 5703 override int maxHeight() { return 16; } 5704 override int minHeight() { return 16; } 5705 } else version(custom_widgets) { 5706 override int maxHeight() { return Window.lineHeight; } 5707 override int minHeight() { return Window.lineHeight; } 5708 } else static assert(0); 5709 5710 override int marginLeft() { return 4; } 5711 5712 private string label; 5713 5714 /// 5715 this(string label, Widget parent = null) { 5716 super(parent); 5717 this.label = label; 5718 version(win32_widgets) { 5719 createWin32Window(this, "button"w, label, BS_CHECKBOX); 5720 } else version(custom_widgets) { 5721 5722 } else static assert(0); 5723 } 5724 5725 version(custom_widgets) 5726 override void paint(WidgetPainter painter) { 5727 if(isFocused()) { 5728 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 5729 painter.fillColor = windowBackgroundColor; 5730 painter.drawRectangle(Point(0, 0), width, height); 5731 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 5732 } else { 5733 painter.pen = Pen(windowBackgroundColor, 1, Pen.Style.Solid); 5734 painter.fillColor = windowBackgroundColor; 5735 painter.drawRectangle(Point(0, 0), width, height); 5736 } 5737 5738 5739 enum buttonSize = 16; 5740 5741 painter.outlineColor = Color.black; 5742 painter.fillColor = Color.white; 5743 painter.drawRectangle(Point(2, 2), buttonSize - 2, buttonSize - 2); 5744 5745 if(isChecked) { 5746 painter.pen = Pen(Color.black, 2); 5747 // I'm using height so the checkbox is square 5748 enum padding = 5; 5749 painter.drawLine(Point(padding, padding), Point(buttonSize - (padding-2), buttonSize - (padding-2))); 5750 painter.drawLine(Point(buttonSize-(padding-2), padding), Point(padding, buttonSize - (padding-2))); 5751 5752 painter.pen = Pen(Color.black, 1); 5753 } 5754 5755 painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 5756 } 5757 5758 override void defaultEventHandler_triggered(Event ev) { 5759 isChecked = !isChecked; 5760 5761 auto event = new Event(EventType.change, this); 5762 event.dispatch(); 5763 5764 redraw(); 5765 } 5766 } 5767 5768 /// Adds empty space to a layout. 5769 class VerticalSpacer : Widget { 5770 /// 5771 this(Widget parent = null) { 5772 super(parent); 5773 } 5774 } 5775 5776 /// ditto 5777 class HorizontalSpacer : Widget { 5778 /// 5779 this(Widget parent = null) { 5780 super(parent); 5781 this.tabStop = false; 5782 } 5783 } 5784 5785 5786 /// 5787 class Radiobox : MouseActivatedWidget { 5788 5789 version(win32_widgets) { 5790 override int maxHeight() { return 16; } 5791 override int minHeight() { return 16; } 5792 } else version(custom_widgets) { 5793 override int maxHeight() { return Window.lineHeight; } 5794 override int minHeight() { return Window.lineHeight; } 5795 } else static assert(0); 5796 5797 override int marginLeft() { return 4; } 5798 5799 private string label; 5800 5801 version(win32_widgets) 5802 this(string label, Widget parent = null) { 5803 super(parent); 5804 this.label = label; 5805 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 5806 } 5807 else version(custom_widgets) 5808 this(string label, Widget parent = null) { 5809 super(parent); 5810 this.label = label; 5811 height = 16; 5812 width = height + 4 + cast(int) label.length * 16; 5813 } 5814 else static assert(false); 5815 5816 version(custom_widgets) 5817 override void paint(WidgetPainter painter) { 5818 if(isFocused) { 5819 painter.fillColor = windowBackgroundColor; 5820 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 5821 } else { 5822 painter.fillColor = windowBackgroundColor; 5823 painter.outlineColor = windowBackgroundColor; 5824 } 5825 painter.drawRectangle(Point(0, 0), width, height); 5826 5827 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 5828 5829 enum buttonSize = 16; 5830 5831 painter.outlineColor = Color.black; 5832 painter.fillColor = Color.white; 5833 painter.drawEllipse(Point(2, 2), Point(buttonSize - 2, buttonSize - 2)); 5834 if(isChecked) { 5835 painter.outlineColor = Color.black; 5836 painter.fillColor = Color.black; 5837 // I'm using height so the checkbox is square 5838 painter.drawEllipse(Point(5, 5), Point(buttonSize - 5, buttonSize - 5)); 5839 } 5840 5841 painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 5842 } 5843 5844 5845 override void defaultEventHandler_triggered(Event ev) { 5846 isChecked = true; 5847 5848 if(this.parent) { 5849 foreach(child; this.parent.children) { 5850 if(child is this) continue; 5851 if(auto rb = cast(Radiobox) child) { 5852 rb.isChecked = false; 5853 auto event = new Event(EventType.change, rb); 5854 event.dispatch(); 5855 rb.redraw(); 5856 } 5857 } 5858 } 5859 5860 auto event = new Event(EventType.change, this); 5861 event.dispatch(); 5862 5863 redraw(); 5864 } 5865 5866 } 5867 5868 5869 /// 5870 class Button : MouseActivatedWidget { 5871 Color normalBgColor; 5872 Color hoverBgColor; 5873 Color depressedBgColor; 5874 5875 override int heightStretchiness() { return 3; } 5876 override int widthStretchiness() { return 3; } 5877 5878 version(win32_widgets) 5879 override void handleWmCommand(ushort cmd, ushort id) { 5880 auto event = new Event("triggered", this); 5881 event.dispatch(); 5882 } 5883 5884 version(win32_widgets) {} 5885 else version(custom_widgets) 5886 Color currentButtonColor() { 5887 if(isHovering) { 5888 return isDepressed ? depressedBgColor : hoverBgColor; 5889 } 5890 5891 return normalBgColor; 5892 } 5893 else static assert(false); 5894 5895 private string label_; 5896 5897 /// 5898 string label() { return label_; } 5899 /// 5900 void label(string l) { 5901 label_ = l; 5902 version(win32_widgets) { 5903 WCharzBuffer bfr = WCharzBuffer(l); 5904 SetWindowTextW(hwnd, bfr.ptr); 5905 } else version(custom_widgets) { 5906 redraw(); 5907 } 5908 } 5909 5910 version(win32_widgets) 5911 this(string label, Widget parent = null) { 5912 // FIXME: use ideal button size instead 5913 width = 50; 5914 height = 30; 5915 super(parent); 5916 createWin32Window(this, "button"w, label, BS_PUSHBUTTON); 5917 5918 this.label = label; 5919 } 5920 else version(custom_widgets) 5921 this(string label, Widget parent = null) { 5922 width = 50; 5923 height = 30; 5924 super(parent); 5925 normalBgColor = buttonColor; 5926 hoverBgColor = hoveringColor; 5927 depressedBgColor = depressedButtonColor; 5928 5929 this.label = label; 5930 } 5931 else static assert(false); 5932 5933 override int minHeight() { return Window.lineHeight + 4; } 5934 5935 version(custom_widgets) 5936 override void paint(WidgetPainter painter) { 5937 this.draw3dFrame(painter, isDepressed ? FrameStyle.sunk : FrameStyle.risen, currentButtonColor); 5938 5939 5940 painter.outlineColor = Color.black; 5941 painter.drawText(Point(0, 0), label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 5942 5943 if(isFocused()) { 5944 painter.fillColor = Color.transparent; 5945 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 5946 painter.drawRectangle(Point(2, 2), width - 4, height - 4); 5947 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 5948 5949 } 5950 } 5951 5952 } 5953 5954 /++ 5955 A button with a consistent size, suitable for user commands like OK and Cancel. 5956 +/ 5957 class CommandButton : Button { 5958 this(string label, Widget parent = null) { 5959 super(label, parent); 5960 } 5961 5962 override int maxHeight() { 5963 return Window.lineHeight + 4; 5964 } 5965 5966 override int maxWidth() { 5967 return Window.lineHeight * 4; 5968 } 5969 5970 override int marginLeft() { return 12; } 5971 override int marginRight() { return 12; } 5972 override int marginTop() { return 12; } 5973 override int marginBottom() { return 12; } 5974 } 5975 5976 /// 5977 enum ArrowDirection { 5978 left, /// 5979 right, /// 5980 up, /// 5981 down /// 5982 } 5983 5984 /// 5985 version(custom_widgets) 5986 class ArrowButton : Button { 5987 /// 5988 this(ArrowDirection direction, Widget parent = null) { 5989 super("", parent); 5990 this.direction = direction; 5991 } 5992 5993 private ArrowDirection direction; 5994 5995 override int minHeight() { return 16; } 5996 override int maxHeight() { return 16; } 5997 override int minWidth() { return 16; } 5998 override int maxWidth() { return 16; } 5999 6000 override void paint(WidgetPainter painter) { 6001 super.paint(painter); 6002 6003 painter.outlineColor = Color.black; 6004 painter.fillColor = Color.black; 6005 6006 auto offset = Point((this.width - 16) / 2, (this.height - 16) / 2); 6007 6008 final switch(direction) { 6009 case ArrowDirection.up: 6010 painter.drawPolygon( 6011 Point(2, 10) + offset, 6012 Point(7, 5) + offset, 6013 Point(12, 10) + offset, 6014 Point(2, 10) + offset 6015 ); 6016 break; 6017 case ArrowDirection.down: 6018 painter.drawPolygon( 6019 Point(2, 6) + offset, 6020 Point(7, 11) + offset, 6021 Point(12, 6) + offset, 6022 Point(2, 6) + offset 6023 ); 6024 break; 6025 case ArrowDirection.left: 6026 painter.drawPolygon( 6027 Point(10, 2) + offset, 6028 Point(5, 7) + offset, 6029 Point(10, 12) + offset, 6030 Point(10, 2) + offset 6031 ); 6032 break; 6033 case ArrowDirection.right: 6034 painter.drawPolygon( 6035 Point(6, 2) + offset, 6036 Point(11, 7) + offset, 6037 Point(6, 12) + offset, 6038 Point(6, 2) + offset 6039 ); 6040 break; 6041 } 6042 } 6043 } 6044 6045 private 6046 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 6047 int x, y; 6048 Widget par = c; 6049 while(par) { 6050 x += par.x; 6051 y += par.y; 6052 par = par.parent; 6053 } 6054 return [x, y]; 6055 } 6056 6057 version(win32_widgets) 6058 private 6059 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 6060 int x, y; 6061 Widget par = c; 6062 while(par) { 6063 x += par.x; 6064 y += par.y; 6065 par = par.parent; 6066 if(par !is null && par.useNativeDrawing()) 6067 break; 6068 } 6069 return [x, y]; 6070 } 6071 6072 /// 6073 class ImageBox : Widget { 6074 private MemoryImage image_; 6075 6076 /// 6077 public void setImage(MemoryImage image){ 6078 this.image_ = image; 6079 if(this.parentWindow && this.parentWindow.win) 6080 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); 6081 redraw(); 6082 } 6083 6084 /// How to fit the image in the box if they aren't an exact match in size? 6085 enum HowToFit { 6086 center, /// centers the image, cropping around all the edges as needed 6087 crop, /// always draws the image in the upper left, cropping the lower right if needed 6088 // stretch, /// not implemented 6089 } 6090 6091 private Sprite sprite; 6092 private HowToFit howToFit_; 6093 6094 private Color backgroundColor_; 6095 6096 /// 6097 this(MemoryImage image, HowToFit howToFit, Color backgroundColor = Color.transparent, Widget parent = null) { 6098 this.image_ = image; 6099 this.tabStop = false; 6100 this.howToFit_ = howToFit; 6101 this.backgroundColor_ = backgroundColor; 6102 super(parent); 6103 updateSprite(); 6104 } 6105 6106 private void updateSprite() { 6107 if(sprite is null && this.parentWindow && this.parentWindow.win) 6108 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); 6109 } 6110 6111 override void paint(WidgetPainter painter) { 6112 updateSprite(); 6113 if(backgroundColor_.a) { 6114 painter.fillColor = backgroundColor_; 6115 painter.drawRectangle(Point(0, 0), width, height); 6116 } 6117 if(howToFit_ == HowToFit.crop) 6118 sprite.drawAt(painter, Point(0, 0)); 6119 else if(howToFit_ == HowToFit.center) { 6120 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 6121 } 6122 } 6123 } 6124 6125 /// 6126 class TextLabel : Widget { 6127 override int maxHeight() { return Window.lineHeight; } 6128 override int minHeight() { return Window.lineHeight; } 6129 override int minWidth() { return 32; } 6130 6131 string label_; 6132 6133 /// 6134 @scriptable 6135 string label() { return label_; } 6136 6137 /// 6138 @scriptable 6139 void label(string l) { 6140 label_ = l; 6141 version(win32_widgets) { 6142 WCharzBuffer bfr = WCharzBuffer(l); 6143 SetWindowTextW(hwnd, bfr.ptr); 6144 } else version(custom_widgets) 6145 redraw(); 6146 } 6147 6148 /// 6149 this(string label, Widget parent = null) { 6150 this(label, TextAlignment.Right, parent); 6151 } 6152 6153 /// 6154 this(string label, TextAlignment alignment, Widget parent = null) { 6155 this.label_ = label; 6156 this.alignment = alignment; 6157 this.tabStop = false; 6158 super(parent); 6159 6160 version(win32_widgets) 6161 createWin32Window(this, "static"w, label, 0, alignment == TextAlignment.Right ? WS_EX_RIGHT : WS_EX_LEFT); 6162 } 6163 6164 TextAlignment alignment; 6165 6166 version(custom_widgets) 6167 override void paint(WidgetPainter painter) { 6168 painter.outlineColor = Color.black; 6169 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 6170 } 6171 6172 } 6173 6174 version(custom_widgets) 6175 private mixin ExperimentalTextComponent; 6176 6177 version(win32_widgets) 6178 alias EditableTextWidgetParent = Widget; /// 6179 else version(custom_widgets) 6180 alias EditableTextWidgetParent = ScrollableWidget; /// 6181 else static assert(0); 6182 6183 /// Contains the implementation of text editing 6184 abstract class EditableTextWidget : EditableTextWidgetParent { 6185 this(Widget parent = null) { 6186 super(parent); 6187 } 6188 6189 bool wordWrapEnabled_ = false; 6190 void wordWrapEnabled(bool enabled) { 6191 version(win32_widgets) { 6192 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 6193 } else version(custom_widgets) { 6194 wordWrapEnabled_ = enabled; // FIXME 6195 } else static assert(false); 6196 } 6197 6198 override int minWidth() { return 16; } 6199 override int minHeight() { return Window.lineHeight + 0; } // the +0 is to leave room for the padding 6200 override int widthStretchiness() { return 7; } 6201 6202 void selectAll() { 6203 version(win32_widgets) 6204 SendMessage(hwnd, EM_SETSEL, 0, -1); 6205 else version(custom_widgets) { 6206 textLayout.selectAll(); 6207 redraw(); 6208 } 6209 } 6210 6211 @property string content() { 6212 version(win32_widgets) { 6213 wchar[4096] bufferstack; 6214 wchar[] buffer; 6215 auto len = GetWindowTextLength(hwnd); 6216 if(len < bufferstack.length) 6217 buffer = bufferstack[0 .. len + 1]; 6218 else 6219 buffer = new wchar[](len + 1); 6220 6221 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 6222 if(l >= 0) 6223 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 6224 else 6225 return null; 6226 } else version(custom_widgets) { 6227 return textLayout.getPlainText(); 6228 } else static assert(false); 6229 } 6230 @property void content(string s) { 6231 version(win32_widgets) { 6232 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 6233 SetWindowTextW(hwnd, bfr.ptr); 6234 } else version(custom_widgets) { 6235 textLayout.clear(); 6236 textLayout.addText(s); 6237 6238 { 6239 // FIXME: it should be able to get this info easier 6240 auto painter = draw(); 6241 textLayout.redoLayout(painter); 6242 } 6243 auto cbb = textLayout.contentBoundingBox(); 6244 setContentSize(cbb.width, cbb.height); 6245 /* 6246 textLayout.addText(ForegroundColor.red, s); 6247 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 6248 textLayout.addText(" is the best!"); 6249 */ 6250 redraw(); 6251 } 6252 else static assert(false); 6253 } 6254 6255 void addText(string txt) { 6256 version(custom_widgets) { 6257 6258 textLayout.addText(txt); 6259 6260 { 6261 // FIXME: it should be able to get this info easier 6262 auto painter = draw(); 6263 textLayout.redoLayout(painter); 6264 } 6265 auto cbb = textLayout.contentBoundingBox(); 6266 setContentSize(cbb.width, cbb.height); 6267 6268 } else version(win32_widgets) { 6269 // get the current selection 6270 DWORD StartPos, EndPos; 6271 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 6272 6273 // move the caret to the end of the text 6274 int outLength = GetWindowTextLengthW(hwnd); 6275 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 6276 6277 // insert the text at the new caret position 6278 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 6279 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 6280 6281 // restore the previous selection 6282 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 6283 } else static assert(0); 6284 } 6285 6286 version(custom_widgets) 6287 override void paintFrameAndBackground(WidgetPainter painter) { 6288 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 6289 } 6290 6291 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 6292 else version(custom_widgets) { 6293 // FIXME 6294 6295 static if(SimpledisplayTimerAvailable) 6296 Timer caretTimer; 6297 TextLayout textLayout; 6298 6299 void setupCustomTextEditing() { 6300 textLayout = new TextLayout(Rectangle(4, 2, width - 8, height - 4)); 6301 } 6302 6303 override void paint(WidgetPainter painter) { 6304 if(parentWindow.win.closed) return; 6305 6306 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 6307 6308 /* 6309 painter.outlineColor = Color.white; 6310 painter.fillColor = Color.white; 6311 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 6312 */ 6313 6314 painter.outlineColor = Color.black; 6315 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 6316 6317 textLayout.caretShowingOnScreen = false; 6318 6319 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 6320 } 6321 6322 6323 override MouseCursor cursor() { 6324 return GenericCursor.Text; 6325 } 6326 } 6327 else static assert(false); 6328 6329 6330 6331 version(custom_widgets) 6332 override void defaultEventHandler_mousedown(Event ev) { 6333 super.defaultEventHandler_mousedown(ev); 6334 if(parentWindow.win.closed) return; 6335 if(ev.button == MouseButton.left) { 6336 if(textLayout.selectNone()) 6337 redraw(); 6338 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 6339 this.focus(); 6340 //this.parentWindow.win.grabInput(); 6341 } else if(ev.button == MouseButton.middle) { 6342 static if(UsingSimpledisplayX11) { 6343 getPrimarySelection(parentWindow.win, (txt) { 6344 textLayout.insert(txt); 6345 redraw(); 6346 6347 auto cbb = textLayout.contentBoundingBox(); 6348 setContentSize(cbb.width, cbb.height); 6349 }); 6350 } 6351 } 6352 } 6353 6354 version(custom_widgets) 6355 override void defaultEventHandler_mouseup(Event ev) { 6356 //this.parentWindow.win.releaseInputGrab(); 6357 super.defaultEventHandler_mouseup(ev); 6358 } 6359 6360 version(custom_widgets) 6361 override void defaultEventHandler_mousemove(Event ev) { 6362 super.defaultEventHandler_mousemove(ev); 6363 if(ev.state & ModifierState.leftButtonDown) { 6364 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 6365 redraw(); 6366 } 6367 } 6368 6369 version(custom_widgets) 6370 override void defaultEventHandler_focus(Event ev) { 6371 super.defaultEventHandler_focus(ev); 6372 if(parentWindow.win.closed) return; 6373 auto painter = this.draw(); 6374 textLayout.drawCaret(painter); 6375 6376 static if(SimpledisplayTimerAvailable) 6377 if(caretTimer) { 6378 caretTimer.destroy(); 6379 caretTimer = null; 6380 } 6381 6382 bool blinkingCaret = true; 6383 static if(UsingSimpledisplayX11) 6384 if(!Image.impl.xshmAvailable) 6385 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 6386 6387 if(blinkingCaret) 6388 static if(SimpledisplayTimerAvailable) 6389 caretTimer = new Timer(500, { 6390 if(parentWindow.win.closed) { 6391 caretTimer.destroy(); 6392 return; 6393 } 6394 if(isFocused()) { 6395 auto painter = this.draw(); 6396 textLayout.drawCaret(painter); 6397 } else if(textLayout.caretShowingOnScreen) { 6398 auto painter = this.draw(); 6399 textLayout.eraseCaret(painter); 6400 } 6401 }); 6402 } 6403 6404 override void defaultEventHandler_blur(Event ev) { 6405 super.defaultEventHandler_blur(ev); 6406 if(parentWindow.win.closed) return; 6407 version(custom_widgets) { 6408 auto painter = this.draw(); 6409 textLayout.eraseCaret(painter); 6410 static if(SimpledisplayTimerAvailable) 6411 if(caretTimer) { 6412 caretTimer.destroy(); 6413 caretTimer = null; 6414 } 6415 } 6416 6417 auto evt = new Event(EventType.change, this); 6418 evt.stringValue = this.content; 6419 evt.dispatch(); 6420 } 6421 6422 version(custom_widgets) 6423 override void defaultEventHandler_char(Event ev) { 6424 super.defaultEventHandler_char(ev); 6425 textLayout.insert(ev.character); 6426 redraw(); 6427 6428 // FIXME: too inefficient 6429 auto cbb = textLayout.contentBoundingBox(); 6430 setContentSize(cbb.width, cbb.height); 6431 } 6432 version(custom_widgets) 6433 override void defaultEventHandler_keydown(Event ev) { 6434 //super.defaultEventHandler_keydown(ev); 6435 switch(ev.key) { 6436 case Key.Delete: 6437 textLayout.delete_(); 6438 redraw(); 6439 break; 6440 case Key.Left: 6441 textLayout.moveLeft(); 6442 redraw(); 6443 break; 6444 case Key.Right: 6445 textLayout.moveRight(); 6446 redraw(); 6447 break; 6448 case Key.Up: 6449 textLayout.moveUp(); 6450 redraw(); 6451 break; 6452 case Key.Down: 6453 textLayout.moveDown(); 6454 redraw(); 6455 break; 6456 case Key.Home: 6457 textLayout.moveHome(); 6458 redraw(); 6459 break; 6460 case Key.End: 6461 textLayout.moveEnd(); 6462 redraw(); 6463 break; 6464 case Key.PageUp: 6465 foreach(i; 0 .. 32) 6466 textLayout.moveUp(); 6467 redraw(); 6468 break; 6469 case Key.PageDown: 6470 foreach(i; 0 .. 32) 6471 textLayout.moveDown(); 6472 redraw(); 6473 break; 6474 6475 default: 6476 {} // intentionally blank, let "char" handle it 6477 } 6478 /* 6479 if(ev.key == Key.Backspace) { 6480 textLayout.backspace(); 6481 redraw(); 6482 } 6483 */ 6484 ensureVisibleInScroll(textLayout.caretBoundingBox()); 6485 } 6486 6487 6488 } 6489 6490 /// 6491 class LineEdit : EditableTextWidget { 6492 // FIXME: hack 6493 version(custom_widgets) { 6494 override bool showingVerticalScroll() { return false; } 6495 override bool showingHorizontalScroll() { return false; } 6496 } 6497 6498 /// 6499 this(Widget parent = null) { 6500 super(parent); 6501 version(win32_widgets) { 6502 createWin32Window(this, "edit"w, "", 6503 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 6504 } else version(custom_widgets) { 6505 setupCustomTextEditing(); 6506 addEventListener("char", delegate(Widget _this, Event ev) { 6507 if(ev.character == '\n') 6508 ev.preventDefault(); 6509 }); 6510 } else static assert(false); 6511 } 6512 override int maxHeight() { return Window.lineHeight + 4; } 6513 override int minHeight() { return Window.lineHeight + 4; } 6514 } 6515 6516 /// 6517 class TextEdit : EditableTextWidget { 6518 /// 6519 this(Widget parent = null) { 6520 super(parent); 6521 version(win32_widgets) { 6522 createWin32Window(this, "edit"w, "", 6523 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 6524 } else version(custom_widgets) { 6525 setupCustomTextEditing(); 6526 } else static assert(false); 6527 } 6528 override int maxHeight() { return int.max; } 6529 override int heightStretchiness() { return 7; } 6530 } 6531 6532 6533 /++ 6534 6535 +/ 6536 version(none) 6537 class RichTextDisplay : Widget { 6538 @property void content(string c) {} 6539 void appendContent(string c) {} 6540 } 6541 6542 /// 6543 class MessageBox : Window { 6544 private string message; 6545 MessageBoxButton buttonPressed = MessageBoxButton.None; 6546 /// 6547 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 6548 super(300, 100); 6549 6550 assert(buttons.length); 6551 assert(buttons.length == buttonIds.length); 6552 6553 this.message = message; 6554 6555 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 6556 6557 int x = this.width / 2 - buttonsWidth / 2; 6558 6559 foreach(idx, buttonText; buttons) { 6560 auto button = new Button(buttonText, this); 6561 button.x = x; 6562 button.y = height - (button.height + 10); 6563 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 6564 this.buttonPressed = buttonIds[idx]; 6565 win.close(); 6566 }; })(idx)); 6567 6568 button.registerMovement(); 6569 x += button.width; 6570 x += 16; 6571 if(idx == 0) 6572 button.focus(); 6573 } 6574 6575 win.show(); 6576 redraw(); 6577 } 6578 6579 override void paint(WidgetPainter painter) { 6580 super.paint(painter); 6581 painter.outlineColor = Color.black; 6582 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 6583 } 6584 6585 // this one is all fixed position 6586 override void recomputeChildLayout() {} 6587 } 6588 6589 /// 6590 enum MessageBoxStyle { 6591 OK, /// 6592 OKCancel, /// 6593 RetryCancel, /// 6594 YesNo, /// 6595 YesNoCancel, /// 6596 RetryCancelContinue /// In a multi-part process, if one part fails, ask the user if you should retry that failed step, cancel the entire process, or just continue with the next step, accepting failure on this step. 6597 } 6598 6599 /// 6600 enum MessageBoxIcon { 6601 None, /// 6602 Info, /// 6603 Warning, /// 6604 Error /// 6605 } 6606 6607 /// Identifies the button the user pressed on a message box. 6608 enum MessageBoxButton { 6609 None, /// The user closed the message box without clicking any of the buttons. 6610 OK, /// 6611 Cancel, /// 6612 Retry, /// 6613 Yes, /// 6614 No, /// 6615 Continue /// 6616 } 6617 6618 6619 /++ 6620 Displays a modal message box, blocking until the user dismisses it. 6621 6622 Returns: the button pressed. 6623 +/ 6624 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 6625 version(win32_widgets) { 6626 WCharzBuffer t = WCharzBuffer(title); 6627 WCharzBuffer m = WCharzBuffer(message); 6628 UINT type; 6629 with(MessageBoxStyle) 6630 final switch(style) { 6631 case OK: type |= MB_OK; break; 6632 case OKCancel: type |= MB_OKCANCEL; break; 6633 case RetryCancel: type |= MB_RETRYCANCEL; break; 6634 case YesNo: type |= MB_YESNO; break; 6635 case YesNoCancel: type |= MB_YESNOCANCEL; break; 6636 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 6637 } 6638 with(MessageBoxIcon) 6639 final switch(icon) { 6640 case None: break; 6641 case Info: type |= MB_ICONINFORMATION; break; 6642 case Warning: type |= MB_ICONWARNING; break; 6643 case Error: type |= MB_ICONERROR; break; 6644 } 6645 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 6646 case IDOK: return MessageBoxButton.OK; 6647 case IDCANCEL: return MessageBoxButton.Cancel; 6648 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 6649 case IDYES: return MessageBoxButton.Yes; 6650 case IDNO: return MessageBoxButton.No; 6651 case IDCONTINUE: return MessageBoxButton.Continue; 6652 default: return MessageBoxButton.None; 6653 } 6654 } else { 6655 string[] buttons; 6656 MessageBoxButton[] buttonIds; 6657 with(MessageBoxStyle) 6658 final switch(style) { 6659 case OK: 6660 buttons = ["OK"]; 6661 buttonIds = [MessageBoxButton.OK]; 6662 break; 6663 case OKCancel: 6664 buttons = ["OK", "Cancel"]; 6665 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 6666 break; 6667 case RetryCancel: 6668 buttons = ["Retry", "Cancel"]; 6669 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 6670 break; 6671 case YesNo: 6672 buttons = ["Yes", "No"]; 6673 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 6674 break; 6675 case YesNoCancel: 6676 buttons = ["Yes", "No", "Cancel"]; 6677 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 6678 break; 6679 case RetryCancelContinue: 6680 buttons = ["Try Again", "Cancel", "Continue"]; 6681 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 6682 break; 6683 } 6684 auto mb = new MessageBox(message, buttons, buttonIds); 6685 EventLoop el = EventLoop.get; 6686 el.run(() { return !mb.win.closed; }); 6687 return mb.buttonPressed; 6688 } 6689 } 6690 6691 /// ditto 6692 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 6693 return messageBox(null, message, style, icon); 6694 } 6695 6696 6697 6698 /// 6699 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 6700 6701 /// 6702 struct EventListener { 6703 Widget widget; 6704 string event; 6705 EventHandler handler; 6706 bool useCapture; 6707 6708 /// 6709 void disconnect() { 6710 widget.removeEventListener(this); 6711 } 6712 } 6713 6714 /// 6715 enum EventType : string { 6716 click = "click", /// 6717 6718 mouseenter = "mouseenter", /// 6719 mouseleave = "mouseleave", /// 6720 mousein = "mousein", /// 6721 mouseout = "mouseout", /// 6722 mouseup = "mouseup", /// 6723 mousedown = "mousedown", /// 6724 mousemove = "mousemove", /// 6725 6726 keydown = "keydown", /// 6727 keyup = "keyup", /// 6728 char_ = "char", /// 6729 6730 focus = "focus", /// 6731 blur = "blur", /// 6732 6733 triggered = "triggered", /// 6734 6735 change = "change", /// 6736 } 6737 6738 /// 6739 class Event { 6740 /// Creates an event without populating any members and without sending it. See [dispatch] 6741 this(string eventName, Widget target) { 6742 this.eventName = eventName; 6743 this.srcElement = target; 6744 } 6745 6746 /// Prevents the default event handler (if there is one) from being called 6747 void preventDefault() { 6748 lastDefaultPrevented = true; 6749 defaultPrevented = true; 6750 } 6751 6752 /// Stops the event propagation immediately. 6753 void stopPropagation() { 6754 propagationStopped = true; 6755 } 6756 6757 private bool defaultPrevented; 6758 private bool propagationStopped; 6759 private string eventName; 6760 6761 Widget srcElement; /// 6762 alias srcElement target; /// 6763 6764 Widget relatedTarget; /// 6765 6766 // for mouse events 6767 int clientX; /// The mouse event location relative to the target widget 6768 int clientY; /// ditto 6769 6770 int viewportX; /// The mouse event location relative to the window origin 6771 int viewportY; /// ditto 6772 6773 int button; /// [MouseEvent.button] 6774 int buttonLinear; /// [MouseEvent.buttonLinear] 6775 6776 // for key events 6777 Key key; /// 6778 6779 KeyEvent originalKeyEvent; 6780 6781 // char character events 6782 dchar character; /// 6783 6784 // for several event types 6785 int state; /// 6786 6787 // for change events 6788 int intValue; /// 6789 string stringValue; /// 6790 6791 bool shiftKey; /// 6792 /++ 6793 NOTE: only set on key events right now 6794 6795 History: 6796 Added April 15, 2020 6797 +/ 6798 bool ctrlKey; 6799 6800 /// ditto 6801 bool altKey; 6802 6803 private bool isBubbling; 6804 6805 private void adjustScrolling() { 6806 version(custom_widgets) { // TEMP 6807 viewportX = clientX; 6808 viewportY = clientY; 6809 if(auto se = cast(ScrollableWidget) srcElement) { 6810 clientX += se.scrollOrigin.x; 6811 clientY += se.scrollOrigin.y; 6812 } 6813 } 6814 } 6815 6816 /// this sends it only to the target. If you want propagation, use dispatch() instead. 6817 void sendDirectly() { 6818 if(srcElement is null) 6819 return; 6820 6821 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 6822 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 6823 6824 adjustScrolling(); 6825 6826 auto e = srcElement; 6827 6828 if(eventName in e.bubblingEventHandlers) 6829 foreach(handler; e.bubblingEventHandlers[eventName]) 6830 handler(e, this); 6831 6832 if(!defaultPrevented) 6833 if(eventName in e.defaultEventHandlers) 6834 e.defaultEventHandlers[eventName](e, this); 6835 } 6836 6837 /// this dispatches the element using the capture -> target -> bubble process 6838 void dispatch() { 6839 if(srcElement is null) 6840 return; 6841 6842 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 6843 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 6844 6845 adjustScrolling(); 6846 // first capture, then bubble 6847 6848 Widget[] chain; 6849 Widget curr = srcElement; 6850 while(curr) { 6851 auto l = curr; 6852 chain ~= l; 6853 curr = curr.parent; 6854 } 6855 6856 isBubbling = false; 6857 6858 foreach_reverse(e; chain) { 6859 if(eventName in e.capturingEventHandlers) 6860 foreach(handler; e.capturingEventHandlers[eventName]) 6861 if(handler !is null) 6862 handler(e, this); 6863 6864 // the default on capture should really be to always do nothing 6865 6866 //if(!defaultPrevented) 6867 // if(eventName in e.defaultEventHandlers) 6868 // e.defaultEventHandlers[eventName](e.element, this); 6869 6870 if(propagationStopped) 6871 break; 6872 } 6873 6874 isBubbling = true; 6875 if(!propagationStopped) 6876 foreach(e; chain) { 6877 if(eventName in e.bubblingEventHandlers) 6878 foreach(handler; e.bubblingEventHandlers[eventName]) 6879 if(handler !is null) 6880 handler(e, this); 6881 6882 if(propagationStopped) 6883 break; 6884 } 6885 6886 if(!defaultPrevented) 6887 foreach(e; chain) { 6888 if(eventName in e.defaultEventHandlers) 6889 e.defaultEventHandlers[eventName](e, this); 6890 } 6891 } 6892 } 6893 6894 private bool isAParentOf(Widget a, Widget b) { 6895 if(a is null || b is null) 6896 return false; 6897 6898 while(b !is null) { 6899 if(a is b) 6900 return true; 6901 b = b.parent; 6902 } 6903 6904 return false; 6905 } 6906 6907 private struct WidgetAtPointResponse { 6908 Widget widget; 6909 int x; 6910 int y; 6911 } 6912 6913 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 6914 assert(starting !is null); 6915 auto child = starting.getChildAtPosition(x, y); 6916 while(child) { 6917 if(child.hidden) 6918 continue; 6919 starting = child; 6920 x -= child.x; 6921 y -= child.y; 6922 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 6923 child = r.widget; 6924 if(child is starting) 6925 break; 6926 } 6927 return WidgetAtPointResponse(starting, x, y); 6928 } 6929 6930 version(win32_widgets) { 6931 import core.sys.windows.commctrl; 6932 6933 pragma(lib, "comctl32"); 6934 shared static this() { 6935 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 6936 INITCOMMONCONTROLSEX ic; 6937 ic.dwSize = cast(DWORD) ic.sizeof; 6938 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 6939 if(!InitCommonControlsEx(&ic)) { 6940 //import std.stdio; writeln("ICC failed"); 6941 } 6942 } 6943 6944 6945 // everything from here is just win32 headers copy pasta 6946 private: 6947 extern(Windows): 6948 6949 alias HANDLE HMENU; 6950 HMENU CreateMenu(); 6951 bool SetMenu(HWND, HMENU); 6952 HMENU CreatePopupMenu(); 6953 enum MF_POPUP = 0x10; 6954 enum MF_STRING = 0; 6955 6956 6957 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 6958 struct INITCOMMONCONTROLSEX { 6959 DWORD dwSize; 6960 DWORD dwICC; 6961 } 6962 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 6963 enum { 6964 IDB_STD_SMALL_COLOR, 6965 IDB_STD_LARGE_COLOR, 6966 IDB_VIEW_SMALL_COLOR = 4, 6967 IDB_VIEW_LARGE_COLOR = 5 6968 } 6969 enum { 6970 STD_CUT, 6971 STD_COPY, 6972 STD_PASTE, 6973 STD_UNDO, 6974 STD_REDOW, 6975 STD_DELETE, 6976 STD_FILENEW, 6977 STD_FILEOPEN, 6978 STD_FILESAVE, 6979 STD_PRINTPRE, 6980 STD_PROPERTIES, 6981 STD_HELP, 6982 STD_FIND, 6983 STD_REPLACE, 6984 STD_PRINT // = 14 6985 } 6986 6987 alias HANDLE HIMAGELIST; 6988 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 6989 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 6990 BOOL ImageList_Destroy(HIMAGELIST); 6991 6992 uint MAKELONG(ushort a, ushort b) { 6993 return cast(uint) ((b << 16) | a); 6994 } 6995 6996 6997 struct TBBUTTON { 6998 int iBitmap; 6999 int idCommand; 7000 BYTE fsState; 7001 BYTE fsStyle; 7002 BYTE[2] bReserved; // FIXME: isn't that different on 64 bit? 7003 DWORD dwData; 7004 int iString; 7005 } 7006 7007 enum { 7008 TB_ADDBUTTONSA = WM_USER + 20, 7009 TB_INSERTBUTTONA = WM_USER + 21, 7010 TB_GETIDEALSIZE = WM_USER + 99, 7011 } 7012 7013 struct SIZE { 7014 LONG cx; 7015 LONG cy; 7016 } 7017 7018 7019 enum { 7020 TBSTATE_CHECKED = 1, 7021 TBSTATE_PRESSED = 2, 7022 TBSTATE_ENABLED = 4, 7023 TBSTATE_HIDDEN = 8, 7024 TBSTATE_INDETERMINATE = 16, 7025 TBSTATE_WRAP = 32 7026 } 7027 7028 7029 7030 enum { 7031 ILC_COLOR = 0, 7032 ILC_COLOR4 = 4, 7033 ILC_COLOR8 = 8, 7034 ILC_COLOR16 = 16, 7035 ILC_COLOR24 = 24, 7036 ILC_COLOR32 = 32, 7037 ILC_COLORDDB = 254, 7038 ILC_MASK = 1, 7039 ILC_PALETTE = 2048 7040 } 7041 7042 7043 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 7044 7045 7046 enum { 7047 TB_ENABLEBUTTON = WM_USER + 1, 7048 TB_CHECKBUTTON, 7049 TB_PRESSBUTTON, 7050 TB_HIDEBUTTON, 7051 TB_INDETERMINATE, // = WM_USER + 5, 7052 TB_ISBUTTONENABLED = WM_USER + 9, 7053 TB_ISBUTTONCHECKED, 7054 TB_ISBUTTONPRESSED, 7055 TB_ISBUTTONHIDDEN, 7056 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 7057 TB_SETSTATE = WM_USER + 17, 7058 TB_GETSTATE = WM_USER + 18, 7059 TB_ADDBITMAP = WM_USER + 19, 7060 TB_DELETEBUTTON = WM_USER + 22, 7061 TB_GETBUTTON, 7062 TB_BUTTONCOUNT, 7063 TB_COMMANDTOINDEX, 7064 TB_SAVERESTOREA, 7065 TB_CUSTOMIZE, 7066 TB_ADDSTRINGA, 7067 TB_GETITEMRECT, 7068 TB_BUTTONSTRUCTSIZE, 7069 TB_SETBUTTONSIZE, 7070 TB_SETBITMAPSIZE, 7071 TB_AUTOSIZE, // = WM_USER + 33, 7072 TB_GETTOOLTIPS = WM_USER + 35, 7073 TB_SETTOOLTIPS = WM_USER + 36, 7074 TB_SETPARENT = WM_USER + 37, 7075 TB_SETROWS = WM_USER + 39, 7076 TB_GETROWS, 7077 TB_GETBITMAPFLAGS, 7078 TB_SETCMDID, 7079 TB_CHANGEBITMAP, 7080 TB_GETBITMAP, 7081 TB_GETBUTTONTEXTA, 7082 TB_REPLACEBITMAP, // = WM_USER + 46, 7083 TB_GETBUTTONSIZE = WM_USER + 58, 7084 TB_SETBUTTONWIDTH = WM_USER + 59, 7085 TB_GETBUTTONTEXTW = WM_USER + 75, 7086 TB_SAVERESTOREW = WM_USER + 76, 7087 TB_ADDSTRINGW = WM_USER + 77, 7088 } 7089 7090 extern(Windows) 7091 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 7092 7093 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 7094 7095 7096 enum { 7097 TB_SETINDENT = WM_USER + 47, 7098 TB_SETIMAGELIST, 7099 TB_GETIMAGELIST, 7100 TB_LOADIMAGES, 7101 TB_GETRECT, 7102 TB_SETHOTIMAGELIST, 7103 TB_GETHOTIMAGELIST, 7104 TB_SETDISABLEDIMAGELIST, 7105 TB_GETDISABLEDIMAGELIST, 7106 TB_SETSTYLE, 7107 TB_GETSTYLE, 7108 //TB_GETBUTTONSIZE, 7109 //TB_SETBUTTONWIDTH, 7110 TB_SETMAXTEXTROWS, 7111 TB_GETTEXTROWS // = WM_USER + 61 7112 } 7113 7114 enum { 7115 CCM_FIRST = 0x2000, 7116 CCM_LAST = CCM_FIRST + 0x200, 7117 CCM_SETBKCOLOR = 8193, 7118 CCM_SETCOLORSCHEME = 8194, 7119 CCM_GETCOLORSCHEME = 8195, 7120 CCM_GETDROPTARGET = 8196, 7121 CCM_SETUNICODEFORMAT = 8197, 7122 CCM_GETUNICODEFORMAT = 8198, 7123 CCM_SETVERSION = 0x2007, 7124 CCM_GETVERSION = 0x2008, 7125 CCM_SETNOTIFYWINDOW = 0x2009 7126 } 7127 7128 7129 enum { 7130 PBM_SETRANGE = WM_USER + 1, 7131 PBM_SETPOS, 7132 PBM_DELTAPOS, 7133 PBM_SETSTEP, 7134 PBM_STEPIT, // = WM_USER + 5 7135 PBM_SETRANGE32 = 1030, 7136 PBM_GETRANGE, 7137 PBM_GETPOS, 7138 PBM_SETBARCOLOR, // = 1033 7139 PBM_SETBKCOLOR = CCM_SETBKCOLOR 7140 } 7141 7142 enum { 7143 PBS_SMOOTH = 1, 7144 PBS_VERTICAL = 4 7145 } 7146 7147 enum { 7148 ICC_LISTVIEW_CLASSES = 1, 7149 ICC_TREEVIEW_CLASSES = 2, 7150 ICC_BAR_CLASSES = 4, 7151 ICC_TAB_CLASSES = 8, 7152 ICC_UPDOWN_CLASS = 16, 7153 ICC_PROGRESS_CLASS = 32, 7154 ICC_HOTKEY_CLASS = 64, 7155 ICC_ANIMATE_CLASS = 128, 7156 ICC_WIN95_CLASSES = 255, 7157 ICC_DATE_CLASSES = 256, 7158 ICC_USEREX_CLASSES = 512, 7159 ICC_COOL_CLASSES = 1024, 7160 ICC_STANDARD_CLASSES = 0x00004000, 7161 } 7162 7163 enum WM_USER = 1024; 7164 } 7165 7166 version(win32_widgets) 7167 pragma(lib, "comdlg32"); 7168 7169 7170 /// 7171 enum GenericIcons : ushort { 7172 None, /// 7173 // these happen to match the win32 std icons numerically if you just subtract one from the value 7174 Cut, /// 7175 Copy, /// 7176 Paste, /// 7177 Undo, /// 7178 Redo, /// 7179 Delete, /// 7180 New, /// 7181 Open, /// 7182 Save, /// 7183 PrintPreview, /// 7184 Properties, /// 7185 Help, /// 7186 Find, /// 7187 Replace, /// 7188 Print, /// 7189 } 7190 7191 /// 7192 void getOpenFileName( 7193 void delegate(string) onOK, 7194 string prefilledName = null, 7195 string[] filters = null 7196 ) 7197 { 7198 return getFileName(true, onOK, prefilledName, filters); 7199 } 7200 7201 /// 7202 void getSaveFileName( 7203 void delegate(string) onOK, 7204 string prefilledName = null, 7205 string[] filters = null 7206 ) 7207 { 7208 return getFileName(false, onOK, prefilledName, filters); 7209 } 7210 7211 void getFileName( 7212 bool openOrSave, 7213 void delegate(string) onOK, 7214 string prefilledName = null, 7215 string[] filters = null, 7216 ) 7217 { 7218 7219 version(win32_widgets) { 7220 import core.sys.windows.commdlg; 7221 /* 7222 Ofn.lStructSize = sizeof(OPENFILENAME); 7223 Ofn.hwndOwner = hWnd; 7224 Ofn.lpstrFilter = szFilter; 7225 Ofn.lpstrFile= szFile; 7226 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 7227 Ofn.lpstrFileTitle = szFileTitle; 7228 Ofn.nMaxFileTitle = sizeof(szFileTitle); 7229 Ofn.lpstrInitialDir = (LPSTR)NULL; 7230 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 7231 Ofn.lpstrTitle = szTitle; 7232 */ 7233 7234 7235 wchar[1024] file = 0; 7236 makeWindowsString(prefilledName, file[]); 7237 OPENFILENAME ofn; 7238 ofn.lStructSize = ofn.sizeof; 7239 ofn.lpstrFile = file.ptr; 7240 ofn.nMaxFile = file.length; 7241 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) { 7242 onOK(makeUtf8StringFromWindowsString(ofn.lpstrFile)); 7243 } 7244 } else version(custom_widgets) { 7245 auto picker = new FilePicker(prefilledName); 7246 picker.onOK = onOK; 7247 picker.show(); 7248 } 7249 } 7250 7251 version(custom_widgets) 7252 private 7253 class FilePicker : Dialog { 7254 void delegate(string) onOK; 7255 LineEdit lineEdit; 7256 this(string prefilledName, Window owner = null) { 7257 super(300, 200, "Choose File..."); // owner); 7258 7259 auto listWidget = new ListWidget(this); 7260 7261 lineEdit = new LineEdit(this); 7262 lineEdit.focus(); 7263 lineEdit.addEventListener("char", (Event event) { 7264 if(event.character == '\t' || event.character == '\n') 7265 event.preventDefault(); 7266 }); 7267 7268 listWidget.addEventListener(EventType.change, () { 7269 foreach(o; listWidget.options) 7270 if(o.selected) 7271 lineEdit.content = o.label; 7272 }); 7273 7274 //version(none) 7275 lineEdit.addEventListener(EventType.keydown, (Event event) { 7276 if(event.key == Key.Tab) { 7277 listWidget.clear(); 7278 7279 string commonPrefix; 7280 auto cnt = lineEdit.content; 7281 if(cnt.length >= 2 && cnt[0 ..2] == "./") 7282 cnt = cnt[2 .. $]; 7283 7284 version(Windows) { 7285 WIN32_FIND_DATA data; 7286 WCharzBuffer search = WCharzBuffer("./" ~ cnt ~ "*"); 7287 auto handle = FindFirstFileW(search.ptr, &data); 7288 scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle); 7289 if(handle is INVALID_HANDLE_VALUE) { 7290 if(GetLastError() == ERROR_FILE_NOT_FOUND) 7291 goto file_not_found; 7292 throw new WindowsApiException("FindFirstFileW"); 7293 } 7294 } else version(Posix) { 7295 import core.sys.posix.dirent; 7296 auto dir = opendir("."); 7297 scope(exit) 7298 if(dir) closedir(dir); 7299 if(dir is null) 7300 throw new ErrnoApiException("opendir"); 7301 7302 auto dirent = readdir(dir); 7303 if(dirent is null) 7304 goto file_not_found; 7305 // filter those that don't start with it, since posix doesn't 7306 // do the * thing itself 7307 while(dirent.d_name[0 .. cnt.length] != cnt[]) { 7308 dirent = readdir(dir); 7309 if(dirent is null) 7310 goto file_not_found; 7311 } 7312 } else static assert(0); 7313 7314 while(true) { 7315 //foreach(string name; dirEntries(".", cnt ~ "*", SpanMode.shallow)) { 7316 version(Windows) { 7317 string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]); 7318 } else version(Posix) { 7319 string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup; 7320 } else static assert(0); 7321 7322 7323 listWidget.addOption(name); 7324 if(commonPrefix is null) 7325 commonPrefix = name; 7326 else { 7327 foreach(idx, char i; name) { 7328 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 7329 commonPrefix = commonPrefix[0 .. idx]; 7330 break; 7331 } 7332 } 7333 } 7334 7335 version(Windows) { 7336 auto ret = FindNextFileW(handle, &data); 7337 if(ret == 0) { 7338 if(GetLastError() == ERROR_NO_MORE_FILES) 7339 break; 7340 throw new WindowsApiException("FindNextFileW"); 7341 } 7342 } else version(Posix) { 7343 dirent = readdir(dir); 7344 if(dirent is null) 7345 break; 7346 7347 while(dirent.d_name[0 .. cnt.length] != cnt[]) { 7348 dirent = readdir(dir); 7349 if(dirent is null) 7350 break; 7351 } 7352 7353 if(dirent is null) 7354 break; 7355 } else static assert(0); 7356 } 7357 if(commonPrefix.length) 7358 lineEdit.content = commonPrefix; 7359 7360 file_not_found: 7361 event.preventDefault(); 7362 } 7363 }); 7364 7365 lineEdit.content = prefilledName; 7366 7367 auto hl = new HorizontalLayout(this); 7368 auto cancelButton = new Button("Cancel", hl); 7369 auto okButton = new Button("OK", hl); 7370 7371 recomputeChildLayout(); // FIXME hack 7372 7373 cancelButton.addEventListener(EventType.triggered, &Cancel); 7374 okButton.addEventListener(EventType.triggered, &OK); 7375 7376 this.addEventListener("keydown", (Event event) { 7377 if(event.key == Key.Enter || event.key == Key.PadEnter) { 7378 event.preventDefault(); 7379 OK(); 7380 } 7381 if(event.key == Key.Escape) 7382 Cancel(); 7383 }); 7384 7385 } 7386 7387 override void OK() { 7388 if(onOK) 7389 onOK(lineEdit.content); 7390 close(); 7391 } 7392 } 7393 7394 /* 7395 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 7396 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 7397 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 7398 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 7399 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 7400 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 7401 http://www.sbin.org/doc/Xlib/chapt_03.html 7402 7403 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 7404 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 7405 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 7406 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 7407 */ 7408 7409 7410 // These are all for setMenuAndToolbarFromAnnotatedCode 7411 /// This item in the menu will be preceded by a separator line 7412 /// Group: generating_from_code 7413 struct separator {} 7414 deprecated("It was misspelled, use separator instead") alias seperator = separator; 7415 /// Program-wide keyboard shortcut to trigger the action 7416 /// Group: generating_from_code 7417 struct accelerator { string keyString; } 7418 /// tells which menu the action will be on 7419 /// Group: generating_from_code 7420 struct menu { string name; } 7421 /// Describes which toolbar section the action appears on 7422 /// Group: generating_from_code 7423 struct toolbar { string groupName; } 7424 /// 7425 /// Group: generating_from_code 7426 struct icon { ushort id; } 7427 /// 7428 /// Group: generating_from_code 7429 struct label { string label; } 7430 /// 7431 /// Group: generating_from_code 7432 struct hotkey { dchar ch; } 7433 /// 7434 /// Group: generating_from_code 7435 struct tip { string tip; } 7436 7437 7438 /++ 7439 Observes and allows inspection of an object via automatic gui 7440 +/ 7441 /// Group: generating_from_code 7442 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 7443 return new ObjectInspectionWindowImpl!(T)(t); 7444 } 7445 7446 class ObjectInspectionWindow : Window { 7447 this(int a, int b, string c) { 7448 super(a, b, c); 7449 } 7450 7451 abstract void readUpdatesFromObject(); 7452 } 7453 7454 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 7455 T t; 7456 this(T t) { 7457 this.t = t; 7458 7459 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 7460 7461 foreach(memberName; __traits(derivedMembers, T)) {{ 7462 alias member = I!(__traits(getMember, t, memberName))[0]; 7463 alias type = typeof(member); 7464 static if(is(type == int)) { 7465 auto le = new LabeledLineEdit(memberName ~ ": ", this); 7466 //le.addEventListener("char", (Event ev) { 7467 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 7468 //ev.preventDefault(); 7469 //}); 7470 le.addEventListener(EventType.change, (Event ev) { 7471 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 7472 }); 7473 7474 updateMemberDelegates[memberName] = () { 7475 le.content = toInternal!string(__traits(getMember, t, memberName)); 7476 }; 7477 } 7478 }} 7479 } 7480 7481 void delegate()[string] updateMemberDelegates; 7482 7483 override void readUpdatesFromObject() { 7484 foreach(k, v; updateMemberDelegates) 7485 v(); 7486 } 7487 } 7488 7489 /++ 7490 Creates a dialog based on a data structure. 7491 7492 --- 7493 dialog((YourStructure value) { 7494 // the user filled in the struct and clicked OK, 7495 // you can check the members now 7496 }); 7497 --- 7498 +/ 7499 /// Group: generating_from_code 7500 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null) { 7501 auto dg = new AutomaticDialog!T(onOK, onCancel); 7502 dg.show(); 7503 } 7504 7505 private static template I(T...) { alias I = T; } 7506 7507 7508 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 7509 if(name == "id") 7510 return allLowerCase ? name : "ID"; 7511 7512 char[160] buffer; 7513 int bufferIndex = 0; 7514 bool shouldCap = true; 7515 bool shouldSpace; 7516 bool lastWasCap; 7517 foreach(idx, char ch; name) { 7518 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 7519 7520 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 7521 if(lastWasCap) { 7522 // two caps in a row, don't change. Prolly acronym. 7523 } else { 7524 if(idx) 7525 shouldSpace = true; // new word, add space 7526 } 7527 7528 lastWasCap = true; 7529 } else { 7530 lastWasCap = false; 7531 } 7532 7533 if(shouldSpace) { 7534 buffer[bufferIndex++] = space; 7535 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 7536 shouldSpace = false; 7537 } 7538 if(shouldCap) { 7539 if(ch >= 'a' && ch <= 'z') 7540 ch -= 32; 7541 shouldCap = false; 7542 } 7543 if(allLowerCase && ch >= 'A' && ch <= 'Z') 7544 ch += 32; 7545 buffer[bufferIndex++] = ch; 7546 } 7547 return buffer[0 .. bufferIndex].idup; 7548 } 7549 7550 7551 7552 class AutomaticDialog(T) : Dialog { 7553 T t; 7554 7555 void delegate(T) onOK; 7556 void delegate() onCancel; 7557 7558 override int paddingTop() { return Window.lineHeight; } 7559 override int paddingBottom() { return Window.lineHeight; } 7560 override int paddingRight() { return Window.lineHeight; } 7561 override int paddingLeft() { return Window.lineHeight; } 7562 7563 7564 this(void delegate(T) onOK, void delegate() onCancel) { 7565 static if(is(T == class)) 7566 t = new T(); 7567 this.onOK = onOK; 7568 this.onCancel = onCancel; 7569 super(400, cast(int)(__traits(allMembers, T).length + 5) * Window.lineHeight, T.stringof); 7570 7571 foreach(memberName; __traits(allMembers, T)) { 7572 alias member = I!(__traits(getMember, t, memberName))[0]; 7573 alias type = typeof(member); 7574 static if(is(type == bool)) { 7575 auto box = new Checkbox(memberName.beautify, this); 7576 box.addEventListener(EventType.change, (Event ev) { 7577 __traits(getMember, t, memberName) = box.isChecked; 7578 }); 7579 } else static if(is(type == string)) { 7580 auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this); 7581 le.addEventListener(EventType.change, (Event ev) { 7582 __traits(getMember, t, memberName) = ev.stringValue; 7583 }); 7584 } else static if(is(type : long)) { 7585 auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this); 7586 le.addEventListener("char", (Event ev) { 7587 if((ev.character < '0' || ev.character > '9') && ev.character != '-') 7588 ev.preventDefault(); 7589 }); 7590 le.addEventListener(EventType.change, (Event ev) { 7591 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 7592 }); 7593 } 7594 } 7595 7596 auto hl = new HorizontalLayout(this); 7597 auto stretch = new HorizontalSpacer(hl); // to right align 7598 auto ok = new CommandButton("OK", hl); 7599 auto cancel = new CommandButton("Cancel", hl); 7600 ok.addEventListener(EventType.triggered, &OK); 7601 cancel.addEventListener(EventType.triggered, &Cancel); 7602 7603 this.addEventListener(EventType.keydown, (Event ev) { 7604 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 7605 ok.focus(); 7606 OK(); 7607 ev.preventDefault(); 7608 } 7609 if(ev.key == Key.Escape) { 7610 Cancel(); 7611 ev.preventDefault(); 7612 } 7613 }); 7614 7615 this.children[0].focus(); 7616 } 7617 7618 override void OK() { 7619 onOK(t); 7620 close(); 7621 } 7622 7623 override void Cancel() { 7624 if(onCancel) 7625 onCancel(); 7626 close(); 7627 } 7628 } 7629 7630 private long stringToLong(string s) { 7631 long ret; 7632 if(s.length == 0) 7633 return ret; 7634 bool negative = s[0] == '-'; 7635 if(negative) 7636 s = s[1 .. $]; 7637 foreach(ch; s) { 7638 if(ch >= '0' && ch <= '9') { 7639 ret *= 10; 7640 ret += ch - '0'; 7641 } 7642 } 7643 if(negative) 7644 ret = -ret; 7645 return ret; 7646 }