1 // Written by Christopher E. Miller
2 // See the included license.txt for copyright and license details.
3 
4 module dfl.tooltip;
5 
6 import core.sys.windows.windows;
7 import core.sys.windows.commctrl;
8 import core.stdc.stdlib;
9 
10 import dfl.application;
11 import dfl.base;
12 import dfl.control;
13 import dfl.exception;
14 import dfl.internal.dlib;
15 import dfl.internal.utf;
16 
17 class ToolTip {
18    package this(DWORD style) {
19       _initCommonControls(ICC_TREEVIEW_CLASSES); // Includes tooltip.
20 
21       hwtt = CreateWindowExA(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
22          _TOOLTIPS_CLASSA.ptr, "", style, 0, 0, 50, 50, null, null, null, null);
23       if (!hwtt) {
24          throw new DflException("Unable to create tooltip");
25       }
26    }
27 
28    this() {
29       this(cast(DWORD) WS_POPUP);
30    }
31 
32    ~this() {
33       removeAll(); // Fixes ref count.
34       DestroyWindow(hwtt);
35    }
36 
37    final @property HWND handle() {
38       return hwtt;
39    }
40 
41    final @property void active(bool byes) {
42       SendMessageA(hwtt, TTM_ACTIVATE, byes, 0); // ?
43       _active = byes;
44    }
45 
46    final @property bool active() {
47       return _active;
48    }
49 
50    // Sets autoPopDelay, initialDelay and reshowDelay.
51    final @property void automaticDelay(DWORD ms) {
52       SendMessageA(hwtt, TTM_SETDELAYTIME, TTDT_AUTOMATIC, ms);
53    }
54 
55    /+
56 
57    final @property DWORD automaticDelay() {
58    }
59    +/
60 
61    final @property void autoPopDelay(DWORD ms) {
62       SendMessageA(hwtt, TTM_SETDELAYTIME, TTDT_AUTOPOP, ms);
63    }
64 
65    /+
66 
67    final @property DWORD autoPopDelay() {
68    }
69    +/
70 
71    final @property void initialDelay(DWORD ms) {
72       SendMessageA(hwtt, TTM_SETDELAYTIME, TTDT_INITIAL, ms);
73    }
74 
75    /+
76 
77    final @property DWORD initialDelay() {
78    }
79    +/
80 
81    final @property void reshowDelay(DWORD ms) {
82       SendMessageA(hwtt, TTM_SETDELAYTIME, TTDT_RESHOW, ms);
83    }
84 
85    /+
86 
87    final @property DWORD reshowDelay() {
88    }
89    +/
90 
91    final @property void showAlways(bool byes) {
92       LONG wl;
93       wl = GetWindowLongA(hwtt, GWL_STYLE);
94       if (byes) {
95          if (wl & TTS_ALWAYSTIP) {
96             return;
97          }
98          wl |= TTS_ALWAYSTIP;
99       } else {
100          if (!(wl & TTS_ALWAYSTIP)) {
101             return;
102          }
103          wl &= ~TTS_ALWAYSTIP;
104       }
105       SetWindowLongA(hwtt, GWL_STYLE, wl);
106    }
107 
108    final @property bool showAlways() {
109       return (GetWindowLongA(hwtt, GWL_STYLE) & TTS_ALWAYSTIP) != 0;
110    }
111 
112    // Remove all tooltip text associated with this instance.
113    final void removeAll() {
114       TOOLINFOA tool;
115       tool.cbSize = TOOLINFOA.sizeof;
116       while (SendMessageA(hwtt, TTM_ENUMTOOLSA, 0, cast(LPARAM)&tool)) {
117          SendMessageA(hwtt, TTM_DELTOOLA, 0, cast(LPARAM)&tool);
118          Application.refCountDec(cast(void*) this);
119       }
120    }
121 
122    // WARNING: possible buffer overflow.
123    final Dstring getToolTip(Control ctrl) {
124       Dstring result;
125       TOOLINFOA tool;
126       tool.cbSize = TOOLINFOA.sizeof;
127       tool.uFlags = TTF_IDISHWND;
128       tool.hwnd = ctrl.handle;
129       tool.uId = cast(UINT) ctrl.handle;
130 
131       if (dfl.internal.utf.useUnicode) {
132          tool.lpszText = cast(typeof(tool.lpszText)) malloc((MAX_TIP_TEXT_LENGTH + 1) * wchar
133             .sizeof);
134          if (!tool.lpszText) {
135             throw new OomException;
136          }
137          scope (exit)
138             free(tool.lpszText);
139          tool.lpszText[0 .. 2] = 0;
140          SendMessageA(hwtt, TTM_GETTEXTW, 0, cast(LPARAM)&tool);
141          if (!(cast(wchar*) tool.lpszText)[0]) {
142             result = null;
143          } else {
144             result = fromUnicodez(cast(wchar*) tool.lpszText);
145          }
146       } else {
147          tool.lpszText = cast(typeof(tool.lpszText)) malloc(MAX_TIP_TEXT_LENGTH + 1);
148          if (!tool.lpszText) {
149             throw new OomException;
150          }
151          scope (exit)
152             free(tool.lpszText);
153          tool.lpszText[0] = 0;
154          SendMessageA(hwtt, TTM_GETTEXTA, 0, cast(LPARAM)&tool);
155          if (!tool.lpszText[0]) {
156             result = null;
157          } else {
158             result = fromAnsiz(tool.lpszText); // Assumes fromAnsiz() copies.
159          }
160       }
161       return result;
162    }
163 
164    final void setToolTip(Control ctrl, Dstring text)
165    in {
166       try {
167          ctrl.createControl();
168       }
169       catch (DThrowable o) {
170          assert(0); // If -ctrl- is a child, make sure the parent is set before setting tool tip text.
171          //throw o;
172       }
173    }
174    body {
175       TOOLINFOA tool;
176       tool.cbSize = TOOLINFOA.sizeof;
177       tool.uFlags = TTF_IDISHWND;
178       tool.hwnd = ctrl.handle;
179       tool.uId = cast(UINT) ctrl.handle;
180 
181       if (!text.length) {
182          if (SendMessageA(hwtt, TTM_GETTOOLINFOA, 0, cast(LPARAM)&tool)) {
183             // Remove.
184 
185             SendMessageA(hwtt, TTM_DELTOOLA, 0, cast(LPARAM)&tool);
186 
187             Application.refCountDec(cast(void*) this);
188          }
189          return;
190       }
191 
192       // Hack to help prevent getToolTip() overflow.
193       if (text.length > MAX_TIP_TEXT_LENGTH) {
194          text = text[0 .. MAX_TIP_TEXT_LENGTH];
195       }
196 
197       if (SendMessageA(hwtt, TTM_GETTOOLINFOA, 0, cast(LPARAM)&tool)) {
198          // Update.
199 
200          if (dfl.internal.utf.useUnicode) {
201             tool.lpszText = cast(typeof(tool.lpszText)) toUnicodez(text);
202             SendMessageA(hwtt, TTM_UPDATETIPTEXTW, 0, cast(LPARAM)&tool);
203          } else {
204             tool.lpszText = cast(typeof(tool.lpszText)) unsafeAnsiz(text);
205             SendMessageA(hwtt, TTM_UPDATETIPTEXTA, 0, cast(LPARAM)&tool);
206          }
207       } else {
208          // Add.
209 
210          /+
211          // TOOLINFOA.rect is ignored if TTF_IDISHWND.
212          tool.rect.left = 0;
213          tool.rect.top = 0;
214          tool.rect.right = ctrl.clientSize.width;
215          tool.rect.bottom = ctrl.clientSize.height;
216          +/
217          tool.uFlags |= TTF_SUBCLASS; // Not a good idea ?
218          LRESULT lr;
219          if (dfl.internal.utf.useUnicode) {
220             tool.lpszText = cast(typeof(tool.lpszText)) toUnicodez(text);
221             lr = SendMessageA(hwtt, TTM_ADDTOOLW, 0, cast(LPARAM)&tool);
222          } else {
223             tool.lpszText = cast(typeof(tool.lpszText)) unsafeAnsiz(text);
224             lr = SendMessageA(hwtt, TTM_ADDTOOLA, 0, cast(LPARAM)&tool);
225          }
226 
227          if (lr) {
228             Application.refCountInc(cast(void*) this);
229          }
230       }
231    }
232 
233 private:
234    enum _TOOLTIPS_CLASSA = "tooltips_class32";
235    enum size_t MAX_TIP_TEXT_LENGTH = 2045;
236 
237    HWND hwtt; // Tooltip control handle.
238    bool _active = true;
239 }