1 // Written by Christopher E. Miller
2 // See the included license.txt for copyright and license details.
3 
4 module dfl.notifyicon;
5 import core.sys.windows.windows;
6 
7 import dfl.application;
8 import dfl.base;
9 import dfl.control;
10 import dfl.drawing;
11 import dfl.event;
12 import dfl.exception;
13 import dfl.form;
14 import dfl.internal.dlib;
15 import dfl.internal.utf;
16 
17 version (DFL_NO_MENUS) {
18 } else {
19    private import dfl.menu;
20 }
21 
22 class NotifyIcon {
23    version (DFL_NO_MENUS) {
24    } else {
25 
26       final @property void contextMenu(ContextMenu menu) {
27          this.cmenu = menu;
28       }
29 
30       final @property ContextMenu contextMenu() {
31          return cmenu;
32       }
33    }
34 
35    final @property void icon(Icon ico) {
36       _icon = ico;
37       nid.hIcon = ico ? ico.handle : null;
38 
39       if (visible) {
40          nid.uFlags = NIF_ICON;
41          Shell_NotifyIcon(NIM_MODIFY, &nid);
42       }
43    }
44 
45    final @property Icon icon() {
46       return _icon;
47    }
48 
49    // Must be less than 64 chars.
50    // To-do: hold reference to setter's string, use that for getter.. ?
51    final @property void text(Dstring txt) {
52       if (txt.length >= nid.szTip.length) {
53          throw new DflException("Notify icon text too long");
54       }
55 
56       // To-do: support Unicode.
57 
58       txt = unsafeAnsi(txt); // ...
59       nid.szTip[txt.length] = 0;
60       // FIX: nid.szTip[0 .. txt.length] = txt[];
61       tipLen = txt.length;
62 
63       if (visible) {
64          nid.uFlags = NIF_TIP;
65          Shell_NotifyIcon(NIM_MODIFY, &nid);
66       }
67    }
68 
69    final @property Dstring text() {
70       //return nid.szTip[0 .. tipLen]; // Returning possibly mutated text!
71       //return nid.szTip[0 .. tipLen].dup;
72       //return nid.szTip[0 .. tipLen].idup; // Needed in D2. Doesn't work in D1.
73       return cast(Dstring) nid.szTip[0 .. tipLen].dup; // Needed in D2. Doesn't work in D1.
74    }
75 
76    final @property void visible(bool byes) {
77       if (byes) {
78          if (!nid.uID) {
79             nid.uID = allocNotifyIconID();
80             assert(nid.uID);
81             allNotifyIcons[nid.uID] = this;
82          }
83 
84          _forceAdd();
85       } else if (nid.uID) {
86          _forceDelete();
87 
88          //delete allNotifyIcons[nid.uID];
89          allNotifyIcons.remove(nid.uID);
90          nid.uID = 0;
91       }
92    }
93 
94    final @property bool visible() {
95       return nid.uID != 0;
96    }
97 
98    final void show() {
99       visible = true;
100    }
101 
102    final void hide() {
103       visible = false;
104    }
105 
106    //EventHandler click;
107    Event!(NotifyIcon, EventArgs) click;
108    //EventHandler doubleClick;
109    Event!(NotifyIcon, EventArgs) doubleClick;
110    //MouseEventHandler mouseDown;
111    Event!(NotifyIcon, MouseEventArgs) mouseDown;
112    //MouseEventHandler mouseUp;
113    Event!(NotifyIcon, MouseEventArgs) mouseUp;
114    //MouseEventHandler mouseMove;
115    Event!(NotifyIcon, MouseEventArgs) mouseMove;
116 
117    this() {
118       if (!ctrlNotifyIcon) {
119          _init();
120       }
121 
122       nid.cbSize = nid.sizeof;
123       nid.hWnd = ctrlNotifyIcon.handle;
124       nid.uID = 0;
125       nid.uCallbackMessage = WM_NOTIFYICON;
126       nid.hIcon = null;
127       nid.szTip[0] = '\0';
128    }
129 
130    ~this() {
131       if (nid.uID) {
132          _forceDelete();
133          //delete allNotifyIcons[nid.uID];
134          allNotifyIcons.remove(nid.uID);
135       }
136 
137       //delete allNotifyIcons[nid.uID];
138       //allNotifyIcons.remove(nid.uID);
139 
140       /+
141       if(!allNotifyIcons.length) {
142          delete ctrlNotifyIcon;
143          ctrlNotifyIcon = null;
144       }
145       +/
146    }
147 
148    // Extra.
149    void minimize(IWindow win) {
150       LONG style;
151       HWND hwnd;
152 
153       hwnd = win.handle;
154       style = GetWindowLongA(hwnd, GWL_STYLE);
155 
156       if (style & WS_VISIBLE) {
157          ShowOwnedPopups(hwnd, FALSE);
158 
159          if (!(style & WS_MINIMIZE) && _animation()) {
160             RECT myRect, areaRect;
161 
162             GetWindowRect(hwnd, &myRect);
163             _area(areaRect);
164             DrawAnimatedRects(hwnd, 3, &myRect, &areaRect);
165          }
166 
167          ShowWindow(hwnd, SW_HIDE);
168       }
169    }
170 
171    // Extra.
172    void restore(IWindow win) {
173       LONG style;
174       HWND hwnd;
175 
176       hwnd = win.handle;
177       style = GetWindowLongA(hwnd, GWL_STYLE);
178 
179       if (!(style & WS_VISIBLE)) {
180          if (style & WS_MINIMIZE) {
181             ShowWindow(hwnd, SW_RESTORE);
182          } else {
183             if (_animation()) {
184                RECT myRect, areaRect;
185 
186                GetWindowRect(hwnd, &myRect);
187                _area(areaRect);
188                DrawAnimatedRects(hwnd, 3, &areaRect, &myRect);
189             }
190 
191             ShowWindow(hwnd, SW_SHOW);
192 
193             ShowOwnedPopups(hwnd, TRUE);
194          }
195       } else {
196          if (style & WS_MINIMIZE) {
197             ShowWindow(hwnd, SW_RESTORE);
198          }
199       }
200 
201       SetForegroundWindow(hwnd);
202    }
203 
204 private:
205 
206    NOTIFYICONDATA nid;
207    int tipLen = 0;
208    version (DFL_NO_MENUS) {
209    } else {
210       ContextMenu cmenu;
211    }
212    Icon _icon;
213 
214    package final void _forceAdd() {
215       nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
216       Shell_NotifyIcon(NIM_ADD, &nid);
217    }
218 
219    package final void _forceDelete() {
220       Shell_NotifyIcon(NIM_DELETE, &nid);
221    }
222 
223    // Returns true if min/restore animation is on.
224    static bool _animation() {
225       ANIMATIONINFO ai;
226 
227       ai.cbSize = ai.sizeof;
228       SystemParametersInfoA(SPI_GETANIMATION, ai.sizeof, &ai, 0);
229 
230       return ai.iMinAnimate ? true : false;
231    }
232 
233    // Gets the tray area.
234    static void _area(out RECT rect) {
235       HWND hwTaskbar, hw;
236 
237       hwTaskbar = FindWindowExA(null, null, "Shell_TrayWnd", null);
238       if (hwTaskbar) {
239          hw = FindWindowExA(hwTaskbar, null, "TrayNotifyWnd", null);
240          if (hw) {
241             GetWindowRect(hw, &rect);
242             return;
243          }
244       }
245 
246       APPBARDATA abd;
247 
248       abd.cbSize = abd.sizeof;
249       if (SHAppBarMessage(ABM_GETTASKBARPOS, &abd)) {
250          switch (abd.uEdge) {
251          case ABE_LEFT:
252          case ABE_RIGHT:
253             rect.top = abd.rc.bottom - 100;
254             rect.bottom = abd.rc.bottom - 16;
255             rect.left = abd.rc.left;
256             rect.right = abd.rc.right;
257             break;
258 
259          case ABE_TOP:
260          case ABE_BOTTOM:
261             rect.top = abd.rc.top;
262             rect.bottom = abd.rc.bottom;
263             rect.left = abd.rc.right - 100;
264             rect.right = abd.rc.right - 16;
265             break;
266 
267          default:
268          }
269       } else if (hwTaskbar) {
270          GetWindowRect(hwTaskbar, &rect);
271          if (rect.right - rect.left > 150) {
272             rect.left = rect.right - 150;
273          }
274          if (rect.bottom - rect.top > 30) {
275             rect.top = rect.bottom - 30;
276          }
277       } else {
278          SystemParametersInfoA(SPI_GETWORKAREA, 0, &rect, 0);
279          rect.left = rect.right - 150;
280          rect.top = rect.bottom - 30;
281       }
282    }
283 }
284 
285 package:
286 
287 enum UINT WM_NOTIFYICON = WM_USER + 34; // -wparam- is id, -lparam- is the mouse message such as WM_LBUTTONDBLCLK.
288 UINT wmTaskbarCreated;
289 NotifyIcon[UINT] allNotifyIcons; // Indexed by ID.
290 UINT lastId = 1;
291 NotifyIconControl ctrlNotifyIcon;
292 
293 class NotifyIconControl : Control {
294    override void createHandle() {
295       //if(created)
296       if (isHandleCreated) {
297          return;
298       }
299 
300       if (killing) {
301       create_err:
302          throw new DflException("Notify icon initialization failure");
303       }
304 
305       Application.creatingControl(this);
306       hwnd = CreateWindowExA(wexstyle, CONTROL_CLASSNAME.ptr, "NotifyIcon", 0,
307          0, 0, 0, 0, null, null, Application.getInstance(), null);
308       if (!hwnd) {
309          goto create_err;
310       }
311    }
312 
313    protected override void wndProc(ref Message msg) {
314       if (msg.msg == WM_NOTIFYICON) {
315          if (cast(UINT) msg.wParam in allNotifyIcons) {
316             NotifyIcon ni;
317             Point pt;
318 
319             ni = allNotifyIcons[cast(UINT) msg.wParam];
320 
321             switch (cast(UINT) msg.lParam) { // msg.
322             case WM_MOUSEMOVE:
323                pt = Cursor.position;
324                ni.mouseMove(ni, new MouseEventArgs(Control.mouseButtons(), 0, pt.x,
325                   pt.y, 0));
326                break;
327 
328             case WM_LBUTTONUP:
329                pt = Cursor.position;
330                ni.mouseUp(ni, new MouseEventArgs(MouseButtons.LEFT, 1, pt.x, pt.y,
331                   0));
332 
333                ni.click(ni, EventArgs.empty);
334                break;
335 
336             case WM_RBUTTONUP:
337                pt = Cursor.position;
338                ni.mouseUp(ni, new MouseEventArgs(MouseButtons.RIGHT, 1, pt.x, pt.y,
339                   0));
340 
341                version (DFL_NO_MENUS) {
342                } else {
343                   if (ni.cmenu) {
344                      ni.cmenu.show(ctrlNotifyIcon, pt);
345                   }
346                }
347                break;
348 
349             case WM_LBUTTONDOWN:
350                pt = Cursor.position;
351                ni.mouseDown(ni, new MouseEventArgs(MouseButtons.LEFT, 0, pt.x, pt.y,
352                   0));
353                break;
354 
355             case WM_RBUTTONDOWN:
356                pt = Cursor.position;
357                ni.mouseDown(ni, new MouseEventArgs(MouseButtons.RIGHT, 0, pt.x, pt.y,
358                   0));
359                break;
360 
361             case WM_LBUTTONDBLCLK:
362                ni.doubleClick(ni, EventArgs.empty);
363                break;
364 
365             default:
366             }
367          }
368       } else if (msg.msg == wmTaskbarCreated) {
369          // Show all visible NotifyIcon's.
370          foreach (NotifyIcon ni; allNotifyIcons) {
371             if (ni.visible) {
372                ni._forceAdd();
373             }
374          }
375       }
376 
377       super.wndProc(msg);
378    }
379 }
380 
381 static ~this() {
382    // Due to all items not being destructed at program exit,
383    // remove all visible notify icons because the OS won't.
384    foreach (NotifyIcon ni; allNotifyIcons) {
385       if (ni.visible) {
386          ni._forceDelete();
387       }
388    }
389 
390    allNotifyIcons = null;
391 }
392 
393 UINT allocNotifyIconID() {
394    UINT prev;
395    prev = lastId;
396    for (;;) {
397       lastId++;
398       if (lastId == ushort.max) {
399          lastId = 1;
400       }
401       if (lastId == prev) {
402          throw new DflException("Too many notify icons");
403       }
404 
405       if (!(lastId in allNotifyIcons)) {
406          break;
407       }
408    }
409    return lastId;
410 }
411 
412 void _init() {
413    wmTaskbarCreated = RegisterWindowMessageA("TaskbarCreated");
414 
415    ctrlNotifyIcon = new NotifyIconControl;
416    ctrlNotifyIcon.visible = false;
417 }