1 // Written by Christopher E. Miller
2 // See the included license.txt for copyright and license details.
3 
4 module dfl.tabcontrol;
5 
6 import core.sys.windows.windows;
7 import core.sys.windows.commctrl;
8 
9 import dfl.application;
10 import dfl.base;
11 import dfl.collections;
12 import dfl.control;
13 import dfl.drawing;
14 import dfl.event;
15 import dfl.exception;
16 import dfl.internal.dlib;
17 import dfl.panel;
18 
19 private extern (Windows) void _initTabcontrol();
20 
21 class TabPage : Panel {
22 
23    this(Dstring tabText) {
24       this();
25 
26       this.text = tabText;
27    }
28 
29    /+
30 
31    this(Object v) { // package
32       this(getObjectString(v));
33    }
34    +/
35 
36    this() {
37       Application.ppin(cast(void*) this);
38 
39       ctrlStyle |= ControlStyles.CONTAINER_CONTROL;
40 
41       wstyle &= ~WS_VISIBLE;
42       cbits &= ~CBits.VISIBLE;
43    }
44 
45    override Dstring toString() {
46       return text;
47    }
48 
49    alias opEquals = Control.opEquals;
50 
51    override Dequ opEquals(Object o) {
52       return text == getObjectString(o);
53    }
54 
55    Dequ opEquals(Dstring val) {
56       return text == val;
57    }
58 
59    alias opCmp = Control.opCmp;
60 
61    override int opCmp(Object o) {
62       return stringICmp(text, getObjectString(o));
63    }
64 
65    int opCmp(Dstring val) {
66       return stringICmp(text, val);
67    }
68 
69    // imageIndex
70 
71    override @property void text(Dstring newText) {
72       // Note: this probably causes toStringz() to be called twice,
73       // allocating 2 of the same string.
74 
75       super.text = newText;
76 
77       if (created) {
78          TabControl tc;
79          tc = cast(TabControl) parent;
80          if (tc) {
81             tc.updateTabText(this, newText);
82          }
83       }
84    }
85 
86    alias text = Panel.text; // Overload with Panel.text.
87 
88    /+
89    final @property void toolTipText(Dstring ttt) {
90       // TODO: ...
91    }
92 
93 
94    final @property Dstring toolTipText() {
95       // TODO: ...
96       return null;
97    }
98    +/
99 
100    /+ package +/ /+ protected +/ override int _rtype() { // package
101       return 4;
102    }
103 
104    protected override void setBoundsCore(int x, int y, int width, int height,
105       BoundsSpecified specified) {
106       assert(0); // Cannot set bounds of TabPage; it is done automatically.
107    }
108 
109    package final @property void realBounds(Rect r) {
110       // DMD 0.124: if I don't put this here, super.setBoundsCore ends up calling setBoundsCore instead of super.setBoundsCore.
111       void delegate(int, int, int, int, BoundsSpecified) _foo = &setBoundsCore;
112 
113       super.setBoundsCore(r.x, r.y, r.width, r.height, BoundsSpecified.ALL);
114    }
115 
116    protected override void setVisibleCore(bool byes) {
117       assert(0); // Cannot set visibility of TabPage; it is done automatically.
118    }
119 
120    package final @property void realVisible(bool byes) {
121       // DMD 0.124: if I don't put this here, super.setVisibleCore ends up calling setVisibleCore instead of super.setVisibleCore.
122       void delegate(bool byes) _foo = &setVisibleCore;
123 
124       super.setVisibleCore(byes);
125    }
126 }
127 
128 package union TcItem {
129    TC_ITEMW tciw;
130    TC_ITEMA tcia;
131    struct {
132       UINT mask;
133       UINT lpReserved1;
134       UINT lpReserved2;
135       private void* pszText;
136       int cchTextMax;
137       int iImage;
138       LPARAM lParam;
139    }
140 }
141 
142 class TabPageCollection {
143    protected this(TabControl owner)
144    in {
145       assert(owner.tchildren is null);
146    }
147    body {
148       tc = owner;
149    }
150 
151 private:
152 
153    TabControl tc;
154    TabPage[] _pages = null;
155 
156    void doPages()
157    in {
158       assert(created);
159    }
160    body {
161       Rect area;
162       area = tc.displayRectangle;
163 
164       Message m;
165       m.hWnd = tc.handle;
166 
167       // Note: duplicate code.
168       //TC_ITEMA tci;
169       TcItem tci;
170       if (dfl.internal.utf.useUnicode) {
171          m.msg = TCM_INSERTITEMW; // <--
172          foreach (int i, TabPage page; _pages) {
173             // TODO: TCIF_RTLREADING flag based on rightToLeft property.
174             tci.mask = TCIF_TEXT | TCIF_PARAM;
175             tci.tciw.pszText = cast(typeof(tci.tciw.pszText)) dfl.internal.utf.toUnicodez(
176                page.text); // <--
177             static assert(tci.lParam.sizeof >= (void*).sizeof);
178             tci.lParam = cast(LPARAM) cast(void*) page;
179 
180             m.wParam = i;
181             m.lParam = cast(LPARAM)&tci.tciw;
182             tc.prevWndProc(m);
183             assert(cast(int) m.result != -1);
184          }
185       } else {
186          m.msg = TCM_INSERTITEMA; // <--
187          foreach (int i, TabPage page; _pages) {
188             // TODO: TCIF_RTLREADING flag based on rightToLeft property.
189             tci.mask = TCIF_TEXT | TCIF_PARAM;
190             tci.tcia.pszText = cast(typeof(tci.tcia.pszText)) dfl.internal.utf.toAnsiz(page.text); // <--
191             static assert(tci.lParam.sizeof >= (void*).sizeof);
192             tci.lParam = cast(LPARAM) cast(void*) page;
193 
194             m.wParam = i;
195             m.lParam = cast(LPARAM)&tci.tcia;
196             tc.prevWndProc(m);
197             assert(cast(int) m.result != -1);
198          }
199       }
200    }
201 
202    package final @property bool created() {
203       return tc && tc.created();
204    }
205 
206    void _added(size_t idx, TabPage val) {
207       if (val.parent) {
208          TabControl tc;
209          tc = cast(TabControl) val.parent;
210          if (tc && tc.tabPages.indexOf(val) != -1) {
211             throw new DflException("TabPage already has a parent");
212          }
213       }
214 
215       //val.realVisible = false;
216       assert(val.visible == false);
217       assert(!(tc is null));
218       val.parent = tc;
219 
220       if (created) {
221          Message m;
222          //TC_ITEMA tci;
223          TcItem tci;
224          // TODO: TCIF_RTLREADING flag based on rightToLeft property.
225          tci.mask = TCIF_TEXT | TCIF_PARAM;
226          static assert(tci.lParam.sizeof >= (void*).sizeof);
227          tci.lParam = cast(LPARAM) cast(void*) val;
228          if (dfl.internal.utf.useUnicode) {
229             tci.tciw.pszText = cast(typeof(tci.tciw.pszText)) dfl.internal.utf.toUnicodez(val.text);
230             m = Message(tc.handle, TCM_INSERTITEMW, idx, cast(LPARAM)&tci.tciw);
231          } else {
232             tci.tcia.pszText = cast(typeof(tci.tcia.pszText)) dfl.internal.utf.toAnsiz(val.text);
233             m = Message(tc.handle, TCM_INSERTITEMA, idx, cast(LPARAM)&tci.tcia);
234          }
235          tc.prevWndProc(m);
236          assert(cast(int) m.result != -1);
237 
238          if (tc.selectedTab is val) {
239             //val.realVisible = true;
240             tc.tabToFront(val);
241          }
242       }
243    }
244 
245    void _removed(size_t idx, TabPage val) {
246       if (size_t.max == idx) { // Clear all.
247          if (created) {
248             Message m;
249             m = Message(tc.handle, TCM_DELETEALLITEMS, 0, 0);
250             tc.prevWndProc(m);
251          }
252       } else {
253          //val.parent = null; // Can't do that.
254 
255          if (created) {
256             Message m;
257             m = Message(tc.handle, TCM_DELETEITEM, idx, 0);
258             tc.prevWndProc(m);
259 
260             // Hide this one.
261             val.realVisible = false;
262 
263             // Show next visible.
264             val = tc.selectedTab;
265             if (val) {
266                tc.tabToFront(val);
267             }
268          }
269       }
270    }
271 
272 public:
273 
274    mixin ListWrapArray!(TabPage, _pages, _blankListCallback!(TabPage), _added,
275       _blankListCallback!(TabPage), _removed, true, false, false, true); // CLEAR_EACH
276 }
277 
278 enum TabAlignment : ubyte {
279    TOP,
280    BOTTOM,
281    LEFT,
282    RIGHT,
283 }
284 
285 enum TabAppearance : ubyte {
286    NORMAL,
287    BUTTONS,
288    FLAT_BUTTONS,
289 }
290 
291 enum TabDrawMode : ubyte {
292    NORMAL,
293    OWNER_DRAW_FIXED,
294 }
295 
296 class TabControlBase : ControlSuperClass {
297    this() {
298       _initTabcontrol();
299 
300       wstyle |= WS_TABSTOP;
301       ctrlStyle |= ControlStyles.SELECTABLE | ControlStyles.CONTAINER_CONTROL;
302       wclassStyle = tabcontrolClassStyle;
303    }
304 
305    final @property void drawMode(TabDrawMode dm) {
306       switch (dm) {
307       case TabDrawMode.OWNER_DRAW_FIXED:
308          _style(wstyle | TCS_OWNERDRAWFIXED);
309          break;
310 
311       case TabDrawMode.NORMAL:
312          _style(wstyle & ~TCS_OWNERDRAWFIXED);
313          break;
314 
315       default:
316          assert(0);
317       }
318 
319       _crecreate();
320    }
321 
322    final @property TabDrawMode drawMode() {
323       if (wstyle & TCS_OWNERDRAWFIXED) {
324          return TabDrawMode.OWNER_DRAW_FIXED;
325       }
326       return TabDrawMode.NORMAL;
327    }
328 
329    override @property Rect displayRectangle() {
330       if (!created) {
331          return super.displayRectangle(); // Hack?
332       } else {
333          RECT drr;
334          Message m;
335          drr.left = 0;
336          drr.top = 0;
337          drr.right = clientSize.width;
338          drr.bottom = clientSize.height;
339          m = Message(hwnd, TCM_ADJUSTRECT, FALSE, cast(LPARAM)&drr);
340          prevWndProc(m);
341          return Rect(&drr);
342       }
343    }
344 
345    protected override @property Size defaultSize() {
346       return Size(200, 200); // ?
347    }
348 
349    final Rect getTabRect(int i) {
350       Rect result;
351 
352       if (created) {
353          RECT rt;
354          Message m;
355          m = Message(hwnd, TCM_GETITEMRECT, cast(WPARAM) i, cast(LPARAM)&rt);
356          prevWndProc(m);
357          if (!m.result) {
358             goto rtfail;
359          }
360          result = Rect(&rt);
361       } else {
362          rtfail: with (result) {
363             x = 0;
364             y = 0;
365             width = 0;
366             height = 0;
367          }
368       }
369 
370       return result;
371    }
372 
373    // drawItem event.
374    //EventHandler selectedIndexChanged;
375    Event!(TabControlBase, EventArgs) selectedIndexChanged;
376    //CancelEventHandler selectedIndexChanging;
377    Event!(TabControlBase, CancelEventArgs) selectedIndexChanging;
378 
379    protected override void createParams(ref CreateParams cp) {
380       super.createParams(cp);
381 
382       cp.className = TABCONTROL_CLASSNAME;
383    }
384 
385    protected void onSelectedIndexChanged(EventArgs ea) {
386       selectedIndexChanged(this, ea);
387    }
388 
389    protected void onSelectedIndexChanging(CancelEventArgs ea) {
390       selectedIndexChanging(this, ea);
391    }
392 
393    protected override void prevWndProc(ref Message msg) {
394       //msg.result = CallWindowProcA(tabcontrolPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam);
395       msg.result = dfl.internal.utf.callWindowProc(tabcontrolPrevWndProc,
396          msg.hWnd, msg.msg, msg.wParam, msg.lParam);
397    }
398 
399    protected override void wndProc(ref Message m) {
400       // TODO: support the tab control messages.
401 
402       switch (m.msg) {
403          /+
404          case WM_SETFOCUS:
405             _exStyle(_exStyle() | WS_EX_CONTROLPARENT);
406             break;
407 
408          case WM_KILLFOCUS:
409             _exStyle(_exStyle() & ~WS_EX_CONTROLPARENT);
410             break;
411             +/
412 
413       case TCM_DELETEALLITEMS:
414          m.result = FALSE;
415          return;
416 
417       case TCM_DELETEITEM:
418          m.result = FALSE;
419          return;
420 
421       case TCM_INSERTITEMA:
422       case TCM_INSERTITEMW:
423          m.result = -1;
424          return;
425 
426          //case TCM_REMOVEIMAGE:
427          // return;
428 
429          //case TCM_SETIMAGELIST:
430          // m.result = cast(LRESULT)null;
431          // return;
432 
433       case TCM_SETITEMA:
434       case TCM_SETITEMW:
435          m.result = FALSE;
436          return;
437 
438       case TCM_SETITEMEXTRA:
439          m.result = FALSE;
440          return;
441 
442       case TCM_SETITEMSIZE:
443          m.result = 0;
444          return;
445 
446       case TCM_SETPADDING:
447          return;
448 
449       case TCM_SETTOOLTIPS:
450          return;
451 
452       default:
453       }
454 
455       super.wndProc(m);
456    }
457 
458    protected override void onReflectedMessage(ref Message m) {
459       super.onReflectedMessage(m);
460 
461       TabPage page;
462       NMHDR* nmh;
463       nmh = cast(NMHDR*) m.lParam;
464 
465       switch (nmh.code) {
466       case TCN_SELCHANGE:
467          onSelectedIndexChanged(EventArgs.empty);
468          break;
469 
470       case TCN_SELCHANGING: {
471             scope CancelEventArgs ea = new CancelEventArgs;
472             onSelectedIndexChanging(ea);
473             if (ea.cancel) {
474                m.result = TRUE; // Prevent change.
475                return;
476             }
477          }
478          m.result = FALSE; // Allow change.
479          return;
480 
481       default:
482       }
483    }
484 }
485 
486 class TabControl : TabControlBase {
487    this() {
488       tchildren = new TabPageCollection(this);
489       _pad = Point(6, 3);
490    }
491 
492    final @property void alignment(TabAlignment talign) {
493       switch (talign) {
494       case TabAlignment.TOP:
495          _style(wstyle & ~(TCS_VERTICAL | TCS_RIGHT | TCS_BOTTOM));
496          break;
497 
498       case TabAlignment.BOTTOM:
499          _style((wstyle & ~(TCS_VERTICAL | TCS_RIGHT)) | TCS_BOTTOM);
500          break;
501 
502       case TabAlignment.LEFT:
503          _style((wstyle & ~(TCS_BOTTOM | TCS_RIGHT)) | TCS_VERTICAL);
504          break;
505 
506       case TabAlignment.RIGHT:
507          _style((wstyle & ~TCS_BOTTOM) | TCS_VERTICAL | TCS_RIGHT);
508          break;
509 
510       default:
511          assert(0);
512       }
513 
514       // Display rectangle changed.
515 
516       if (created && visible) {
517          invalidate(true); // Update children too ?
518 
519          TabPage page;
520          page = selectedTab;
521          if (page) {
522             page.realBounds = displayRectangle;
523          }
524       }
525    }
526 
527    final @property TabAlignment alignment() {
528       // Note: TCS_RIGHT and TCS_BOTTOM are the same flag.
529 
530       if (wstyle & TCS_VERTICAL) {
531          if (wstyle & TCS_RIGHT) {
532             return TabAlignment.RIGHT;
533          }
534          return TabAlignment.LEFT;
535       } else {
536          if (wstyle & TCS_BOTTOM) {
537             return TabAlignment.BOTTOM;
538          }
539          return TabAlignment.TOP;
540       }
541    }
542 
543    final @property void appearance(TabAppearance tappear) {
544       switch (tappear) {
545       case TabAppearance.NORMAL:
546          _style(wstyle & ~(TCS_BUTTONS | TCS_FLATBUTTONS));
547          break;
548 
549       case TabAppearance.BUTTONS:
550          _style((wstyle & ~TCS_FLATBUTTONS) | TCS_BUTTONS);
551          break;
552 
553       case TabAppearance.FLAT_BUTTONS:
554          _style(wstyle | TCS_BUTTONS | TCS_FLATBUTTONS);
555          break;
556 
557       default:
558          assert(0);
559       }
560 
561       if (created && visible) {
562          invalidate(false);
563 
564          TabPage page;
565          page = selectedTab;
566          if (page) {
567             page.realBounds = displayRectangle;
568          }
569       }
570    }
571 
572    final @property TabAppearance appearance() {
573       if (wstyle & TCS_FLATBUTTONS) {
574          return TabAppearance.FLAT_BUTTONS;
575       }
576       if (wstyle & TCS_BUTTONS) {
577          return TabAppearance.BUTTONS;
578       }
579       return TabAppearance.NORMAL;
580    }
581 
582    final @property void padding(Point pad) {
583       if (created) {
584          SendMessageA(hwnd, TCM_SETPADDING, 0, MAKELPARAM(pad.x, pad.y));
585 
586          TabPage page;
587          page = selectedTab;
588          if (page) {
589             page.realBounds = displayRectangle;
590          }
591       }
592 
593       _pad = pad;
594    }
595 
596    final @property Point padding() {
597       return _pad;
598    }
599 
600    final @property TabPageCollection tabPages() {
601       return tchildren;
602    }
603 
604    final @property void multiline(bool byes) {
605       if (byes) {
606          _style(_style() | TCS_MULTILINE);
607       } else {
608          _style(_style() & ~TCS_MULTILINE);
609       }
610 
611       TabPage page;
612       page = selectedTab;
613       if (page) {
614          page.realBounds = displayRectangle;
615       }
616    }
617 
618    final @property bool multiline() {
619       return (_style() & TCS_MULTILINE) != 0;
620    }
621 
622    final @property int rowCount() {
623       if (!created || !multiline) {
624          return 0;
625       }
626       Message m;
627       m = Message(hwnd, TCM_GETROWCOUNT, 0, 0);
628       prevWndProc(m);
629       return cast(int) m.result;
630    }
631 
632    final @property int tabCount() {
633       return tchildren._pages.length;
634    }
635 
636    final @property void selectedIndex(int i) {
637       if (!created || !tchildren._pages.length) {
638          return;
639       }
640 
641       TabPage curpage;
642       curpage = selectedTab;
643       if (curpage is tchildren._pages[i]) {
644          return; // Already selected.
645       }
646       curpage.realVisible = false;
647 
648       SendMessageA(hwnd, TCM_SETCURSEL, cast(WPARAM) i, 0);
649       tabToFront(tchildren._pages[i]);
650    }
651 
652    // Returns -1 if there are no tabs selected.
653    final @property int selectedIndex() {
654       if (!created || !tchildren._pages.length) {
655          return -1;
656       }
657       Message m;
658       m = Message(hwnd, TCM_GETCURSEL, 0, 0);
659       prevWndProc(m);
660       return cast(int) m.result;
661    }
662 
663    final @property void selectedTab(TabPage page) {
664       int i;
665       i = tabPages.indexOf(page);
666       if (-1 != i) {
667          selectedIndex = i;
668       }
669    }
670 
671    final @property TabPage selectedTab() {
672       int i;
673       i = selectedIndex;
674       if (-1 == i) {
675          return null;
676       }
677       return tchildren._pages[i];
678    }
679 
680    /+
681 
682    final @property void showToolTips(bool byes) {
683       if(byes) {
684          _style(_style() | TCS_TOOLTIPS);
685       } else {
686          _style(_style() & ~TCS_TOOLTIPS);
687       }
688    }
689 
690 
691    final @property bool showToolTips() {
692       return (_style() & TCS_TOOLTIPS) != 0;
693    }
694    +/
695 
696    protected override void onHandleCreated(EventArgs ea) {
697       super.onHandleCreated(ea);
698 
699       SendMessageA(hwnd, TCM_SETPADDING, 0, MAKELPARAM(_pad.x, _pad.y));
700 
701       tchildren.doPages();
702 
703       // Bring selected tab to front.
704       if (tchildren._pages.length) {
705          int i;
706          i = selectedIndex;
707          if (-1 != i) {
708             tabToFront(tchildren._pages[i]);
709          }
710       }
711    }
712 
713    protected override void onLayout(LayoutEventArgs ea) {
714       if (tchildren._pages.length) {
715          int i;
716          i = selectedIndex;
717          if (-1 != i) {
718             tchildren._pages[i].realBounds = displayRectangle;
719             //assert(tchildren._pages[i].bounds == displayRectangle);
720          }
721       }
722 
723       //super.onLayout(ea); // Tab control shouldn't even have other controls on it.
724       super.onLayout(ea); // Should call it for consistency. Ideally it just checks handlers.length == 0 and does nothing.
725    }
726 
727    /+
728    protected override void wndProc(ref Message m) {
729       // TODO: support the tab control messages.
730 
731       switch(m.msg) {
732             /+ // Now handled in onLayout().
733          case WM_WINDOWPOSCHANGED: {
734                WINDOWPOS* wp;
735                wp = cast(WINDOWPOS*)m.lParam;
736 
737                if(!(wp.flags & SWP_NOSIZE) || (wp.flags & SWP_FRAMECHANGED)) {
738                   if(tchildren._pages.length) {
739                      int i;
740                      i = selectedIndex;
741                      if(-1 != i) {
742                         tchildren._pages[i].realBounds = displayRectangle;
743                         //assert(tchildren._pages[i].bounds == displayRectangle);
744                      }
745                   }
746                }
747             }
748             break;
749             +/
750 
751          default:
752       }
753 
754       super.wndProc(m);
755    }
756    +/
757 
758    protected override void onReflectedMessage(ref Message m) {
759       TabPage page;
760       NMHDR* nmh;
761       nmh = cast(NMHDR*) m.lParam;
762 
763       switch (nmh.code) {
764       case TCN_SELCHANGE:
765          page = selectedTab;
766          if (page) {
767             tabToFront(page);
768          }
769          super.onReflectedMessage(m);
770          break;
771 
772       case TCN_SELCHANGING:
773          super.onReflectedMessage(m);
774          if (!m.result) { // Allowed.
775             page = selectedTab;
776             if (page) {
777                page.realVisible = false;
778             }
779          }
780          return;
781 
782       default:
783          super.onReflectedMessage(m);
784       }
785    }
786 
787    /+
788    /+ package +/ /+ protected +/ override int _rtype() { // package
789       return 0x20;
790    }
791    +/
792 
793 private:
794    Point _pad;
795    TabPageCollection tchildren;
796 
797    void tabToFront(TabPage page) {
798       page.realBounds = displayRectangle;
799       //page.realVisible = true;
800       SetWindowPos(page.handle, HWND_TOP, 0, 0, 0, 0, /+ SWP_NOACTIVATE | +/ SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
801       assert(page.visible == true);
802 
803       /+
804       // Make sure the previous tab isn't still focused.
805       // Will "steal" focus if done programatically.
806       SetFocus(handle);
807       //SetFocus(page.handle);
808       +/
809    }
810 
811    void updateTabText(TabPage page, Dstring newText)
812    in {
813       assert(created);
814    }
815    body {
816       int i;
817       i = tabPages.indexOf(page);
818       assert(-1 != i);
819 
820       //TC_ITEMA tci;
821       TcItem tci;
822       tci.mask = TCIF_TEXT;
823       Message m;
824       if (dfl.internal.utf.useUnicode) {
825          tci.tciw.pszText = cast(typeof(tci.tciw.pszText)) dfl.internal.utf.toUnicodez(newText);
826          m = Message(hwnd, TCM_SETITEMW, cast(WPARAM) i, cast(LPARAM)&tci.tciw);
827       } else {
828          tci.tcia.pszText = cast(typeof(tci.tcia.pszText)) dfl.internal.utf.toAnsiz(newText);
829          m = Message(hwnd, TCM_SETITEMA, cast(WPARAM) i, cast(LPARAM)&tci.tcia);
830       }
831       prevWndProc(m);
832 
833       // Updating a tab's text could cause tab rows to be adjusted,
834       // so update the selected tab's area.
835       page = selectedTab;
836       if (page) {
837          page.realBounds = displayRectangle;
838       }
839    }
840 }