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