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