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