Selective Sync now shows both server AND local folders
[pithos-ms-client] / trunk / NotifyIconWpf / TaskbarIcon.cs
1 // hardcodet.net NotifyIcon for WPF
2 // Copyright (c) 2009 Philipp Sumi
3 // Contact and Information: http://www.hardcodet.net
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the Code Project Open License (CPOL);
7 // either version 1.0 of the License, or (at your option) any later
8 // version.
9 // 
10 // The above copyright notice and this permission notice shall be
11 // included in all copies or substantial portions of the Software.
12 // 
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
15 // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18 // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
20 // OTHER DEALINGS IN THE SOFTWARE.
21 //
22 // THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE
23
24
25 using System;
26 using System.ComponentModel;
27 using System.Diagnostics;
28 using System.Drawing;
29 using System.Threading;
30 using System.Windows;
31 using System.Windows.Controls;
32 using System.Windows.Controls.Primitives;
33 using System.Windows.Interop;
34 using System.Windows.Threading;
35 using Hardcodet.Wpf.TaskbarNotification.Interop;
36 using Point=Hardcodet.Wpf.TaskbarNotification.Interop.Point;
37
38
39
40 namespace Hardcodet.Wpf.TaskbarNotification
41 {
42   /// <summary>
43   /// A WPF proxy to for a taskbar icon (NotifyIcon) that sits in the system's
44   /// taskbar notification area ("system tray").
45   /// </summary>
46   public partial class TaskbarIcon : FrameworkElement, IDisposable
47   {
48     #region Members
49
50     /// <summary>
51     /// Represents the current icon data.
52     /// </summary>
53     private NotifyIconData iconData;
54
55     /// <summary>
56     /// Receives messages from the taskbar icon.
57     /// </summary>
58     private readonly WindowMessageSink messageSink;
59
60     /// <summary>
61     /// An action that is being invoked if the
62     /// <see cref="singleClickTimer"/> fires.
63     /// </summary>
64     private Action delayedTimerAction;
65
66     /// <summary>
67     /// A timer that is used to differentiate between single
68     /// and double clicks.
69     /// </summary>
70     private readonly Timer singleClickTimer;
71
72     /// <summary>
73     /// A timer that is used to close open balloon tooltips.
74     /// </summary>
75     private readonly Timer balloonCloseTimer;
76
77     /// <summary>
78     /// Indicates whether the taskbar icon has been created or not.
79     /// </summary>
80     public bool IsTaskbarIconCreated { get; private set; }
81
82     /// <summary>
83     /// Indicates whether custom tooltips are supported, which depends
84     /// on the OS. Windows Vista or higher is required in order to
85     /// support this feature.
86     /// </summary>
87     public bool SupportsCustomToolTips
88     {
89       get { return messageSink.Version == NotifyIconVersion.Vista; }
90     }
91
92
93
94     /// <summary>
95     /// Checks whether a non-tooltip popup is currently opened.
96     /// </summary>
97     private bool IsPopupOpen
98     {
99       get
100       {
101         var popup = TrayPopupResolved;
102         var menu = ContextMenu;
103         var balloon = CustomBalloon;
104
105         return popup != null && popup.IsOpen ||
106                menu != null && menu.IsOpen ||
107                balloon != null && balloon.IsOpen;
108
109       }
110     }
111
112     #endregion
113
114
115     #region Construction
116
117     /// <summary>
118     /// Inits the taskbar icon and registers a message listener
119     /// in order to receive events from the taskbar area.
120     /// </summary>
121     public TaskbarIcon()
122     {
123       //using dummy sink in design mode
124       messageSink = Util.IsDesignMode
125                         ? WindowMessageSink.CreateEmpty()
126                         : new WindowMessageSink(NotifyIconVersion.Win95);
127
128       //init icon data structure
129       iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle);
130
131       //create the taskbar icon
132       CreateTaskbarIcon();
133
134       //register event listeners
135       messageSink.MouseEventReceived += OnMouseEvent;
136       messageSink.TaskbarCreated += OnTaskbarCreated;
137       messageSink.ChangeToolTipStateRequest += OnToolTipChange;
138       messageSink.BalloonToolTipChanged += OnBalloonToolTipChanged;
139
140       //init single click / balloon timers
141       singleClickTimer = new Timer(DoSingleClickAction);
142       balloonCloseTimer = new Timer(CloseBalloonCallback);
143
144       //register listener in order to get notified when the application closes
145       if (Application.Current != null) Application.Current.Exit += OnExit;
146     }
147
148     #endregion
149
150
151     #region Custom Balloons
152
153     /// <summary>
154     /// Shows a custom control as a tooltip in the tray location.
155     /// </summary>
156     /// <param name="balloon"></param>
157     /// <param name="animation">An optional animation for the popup.</param>
158     /// <param name="timeout">The time after which the popup is being closed.
159     /// Submit null in order to keep the balloon open inde
160     /// </param>
161     /// <exception cref="ArgumentNullException">If <paramref name="balloon"/>
162     /// is a null reference.</exception>
163     public void ShowCustomBalloon(UIElement balloon, PopupAnimation animation, int? timeout)
164     {
165       Dispatcher dispatcher = this.GetDispatcher();
166       if (!dispatcher.CheckAccess())
167       {
168         var action = new Action(() => ShowCustomBalloon(balloon, animation, timeout));
169         dispatcher.Invoke(DispatcherPriority.Normal, action);
170         return;
171       }
172
173       if (balloon == null) throw new ArgumentNullException("balloon");
174       if (timeout.HasValue && timeout < 500)
175       {
176         string msg = "Invalid timeout of {0} milliseconds. Timeout must be at least 500 ms";
177         msg = String.Format(msg, timeout); 
178         throw new ArgumentOutOfRangeException("timeout", msg);
179       }
180
181       EnsureNotDisposed();
182
183       //make sure we don't have an open balloon
184       lock (this)
185       {
186         CloseBalloon();
187       }
188       
189       //create an invisible popup that hosts the UIElement
190       Popup popup = new Popup();
191       popup.AllowsTransparency = true;
192
193       //provide the popup with the taskbar icon's data context
194       UpdateDataContext(popup, null, DataContext);
195
196       //don't animate by default - devs can use attached
197       //events or override
198       popup.PopupAnimation = animation;
199
200       popup.Child = balloon;
201
202       //don't set the PlacementTarget as it causes the popup to become hidden if the
203       //TaskbarIcon's parent is hidden, too...
204       //popup.PlacementTarget = this;
205       
206       popup.Placement = PlacementMode.AbsolutePoint;
207       popup.StaysOpen = true;
208
209       Point position = TrayInfo.GetTrayLocation();
210       popup.HorizontalOffset = position.X -1;
211       popup.VerticalOffset = position.Y -1;
212
213       //store reference
214       lock (this)
215       {
216         SetCustomBalloon(popup);
217       }
218
219       //assign this instance as an attached property
220       SetParentTaskbarIcon(balloon, this);
221
222       //fire attached event
223       RaiseBalloonShowingEvent(balloon, this);
224
225       //display item
226       popup.IsOpen = true;
227
228       if (timeout.HasValue)
229       {
230         //register timer to close the popup
231         balloonCloseTimer.Change(timeout.Value, Timeout.Infinite);
232       }
233     }
234
235
236     /// <summary>
237     /// Resets the closing timeout, which effectively
238     /// keeps a displayed balloon message open until
239     /// it is either closed programmatically through
240     /// <see cref="CloseBalloon"/> or due to a new
241     /// message being displayed.
242     /// </summary>
243     public void ResetBalloonCloseTimer()
244     {
245       if (IsDisposed) return;
246
247       lock (this)
248       {
249         //reset timer in any case
250         balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite);
251       }
252     }
253
254
255     /// <summary>
256     /// Closes the current <see cref="CustomBalloon"/>, if the
257     /// property is set.
258     /// </summary>
259     public void CloseBalloon()
260     {
261       if (IsDisposed) return;
262
263       Dispatcher dispatcher = this.GetDispatcher();
264       if (!dispatcher.CheckAccess())
265       {
266         Action action = CloseBalloon;
267         dispatcher.Invoke(DispatcherPriority.Normal, action);
268         return;
269       }
270
271       lock (this)
272       {
273         //reset timer in any case
274         balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite);
275
276         //reset old popup, if we still have one
277         Popup popup = CustomBalloon;
278         if (popup != null)
279         {
280           UIElement element = popup.Child;
281
282           //announce closing
283           RoutedEventArgs eventArgs = RaiseBalloonClosingEvent(element, this);
284           if (!eventArgs.Handled)
285           {
286             //if the event was handled, clear the reference to the popup,
287             //but don't close it - the handling code has to manage this stuff now
288
289             //close the popup
290             popup.IsOpen = false;
291
292             //reset attached property
293             if (element != null) SetParentTaskbarIcon(element, null);
294           }
295
296           //remove custom balloon anyway
297           SetCustomBalloon(null);
298         }
299       }
300     }
301
302
303     /// <summary>
304     /// Timer-invoke event which closes the currently open balloon and
305     /// resets the <see cref="CustomBalloon"/> dependency property.
306     /// </summary>
307     private void CloseBalloonCallback(object state)
308     {
309       if (IsDisposed) return;
310
311       //switch to UI thread
312       Action action = CloseBalloon;
313       this.GetDispatcher().Invoke(action);
314     }
315
316     #endregion
317
318     #region Process Incoming Mouse Events
319
320     /// <summary>
321     /// Processes mouse events, which are bubbled
322     /// through the class' routed events, trigger
323     /// certain actions (e.g. show a popup), or
324     /// both.
325     /// </summary>
326     /// <param name="me">Event flag.</param>
327     private void OnMouseEvent(MouseEvent me)
328     {
329       if (IsDisposed) return;
330
331       switch (me)
332       {
333         case MouseEvent.MouseMove:
334           RaiseTrayMouseMoveEvent();
335           //immediately return - there's nothing left to evaluate
336           return;
337         case MouseEvent.IconRightMouseDown:
338           RaiseTrayRightMouseDownEvent();
339           break;
340         case MouseEvent.IconLeftMouseDown:
341           RaiseTrayLeftMouseDownEvent();
342           break;
343         case MouseEvent.IconRightMouseUp:
344           RaiseTrayRightMouseUpEvent();
345           break;
346         case MouseEvent.IconLeftMouseUp:
347           RaiseTrayLeftMouseUpEvent();
348           break;
349         case MouseEvent.IconMiddleMouseDown:
350           RaiseTrayMiddleMouseDownEvent();
351           break;
352         case MouseEvent.IconMiddleMouseUp:
353           RaiseTrayMiddleMouseUpEvent();
354           break;
355         case MouseEvent.IconDoubleClick:
356           //cancel single click timer
357           singleClickTimer.Change(Timeout.Infinite, Timeout.Infinite);
358           //bubble event
359           RaiseTrayMouseDoubleClickEvent();
360           break;
361         case MouseEvent.BalloonToolTipClicked:
362           RaiseTrayBalloonTipClickedEvent();
363           break;
364         default:
365           throw new ArgumentOutOfRangeException("me", "Missing handler for mouse event flag: " + me);
366       }
367
368
369       //get mouse coordinates
370       Point cursorPosition = new Point();
371       WinApi.GetCursorPos(ref cursorPosition);
372
373       bool isLeftClickCommandInvoked = false;
374       
375       //show popup, if requested
376       if (me.IsMatch(PopupActivation))
377       {
378         if (me == MouseEvent.IconLeftMouseUp)
379         {
380           //show popup once we are sure it's not a double click
381           delayedTimerAction = () =>
382                                  {
383                                    LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this);
384                                    ShowTrayPopup(cursorPosition);
385                                  };
386           singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite);
387           isLeftClickCommandInvoked = true;
388         }
389         else
390         {
391           //show popup immediately
392           ShowTrayPopup(cursorPosition);
393         }
394       }
395
396
397       //show context menu, if requested
398       if (me.IsMatch(MenuActivation))
399       {
400         if (me == MouseEvent.IconLeftMouseUp)
401         {
402           //show context menu once we are sure it's not a double click
403           delayedTimerAction = () =>
404                                  {
405                                    LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this);
406                                    ShowContextMenu(cursorPosition);
407                                  };
408           singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite);
409           isLeftClickCommandInvoked = true;
410         }
411         else
412         {
413           //show context menu immediately
414           ShowContextMenu(cursorPosition);
415         }
416       }
417
418       //make sure the left click command is invoked on mouse clicks
419       if (me == MouseEvent.IconLeftMouseUp && !isLeftClickCommandInvoked)
420       {
421         //show context menu once we are sure it's not a double click
422         delayedTimerAction = () => LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this);
423         singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite);
424       }
425
426     }
427
428     #endregion
429
430     #region ToolTips
431
432     /// <summary>
433     /// Displays a custom tooltip, if available. This method is only
434     /// invoked for Windows Vista and above.
435     /// </summary>
436     /// <param name="visible">Whether to show or hide the tooltip.</param>
437     private void OnToolTipChange(bool visible)
438     {
439       //if we don't have a tooltip, there's nothing to do here...
440       if (TrayToolTipResolved == null) return;
441
442       if (visible)
443       {
444         if (IsPopupOpen)
445         {
446           //ignore if we are already displaying something down there
447           return;
448         }
449
450         var args = RaisePreviewTrayToolTipOpenEvent();
451         if (args.Handled) return;
452
453         TrayToolTipResolved.IsOpen = true;
454
455         //raise attached event first
456         if (TrayToolTip != null) RaiseToolTipOpenedEvent(TrayToolTip);
457         
458         //bubble routed event
459         RaiseTrayToolTipOpenEvent();
460       }
461       else
462       {
463         var args = RaisePreviewTrayToolTipCloseEvent();
464         if (args.Handled) return;
465
466         //raise attached event first
467         if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip);
468
469         TrayToolTipResolved.IsOpen = false;
470
471         //bubble event
472         RaiseTrayToolTipCloseEvent();
473       }
474     }
475
476
477     /// <summary>
478     /// Creates a <see cref="ToolTip"/> control that either
479     /// wraps the currently set <see cref="TrayToolTip"/>
480     /// control or the <see cref="ToolTipText"/> string.<br/>
481     /// If <see cref="TrayToolTip"/> itself is already
482     /// a <see cref="ToolTip"/> instance, it will be used directly.
483     /// </summary>
484     /// <remarks>We use a <see cref="ToolTip"/> rather than
485     /// <see cref="Popup"/> because there was no way to prevent a
486     /// popup from causing cyclic open/close commands if it was
487     /// placed under the mouse. ToolTip internally uses a Popup of
488     /// its own, but takes advance of Popup's internal <see cref="Popup.HitTestable"/>
489     /// property which prevents this issue.</remarks>
490     private void CreateCustomToolTip()
491     {
492       //check if the item itself is a tooltip
493       ToolTip tt = TrayToolTip as ToolTip;
494
495       if (tt == null && TrayToolTip != null)
496       {
497         //create an invisible tooltip that hosts the UIElement
498         tt = new ToolTip();
499         tt.Placement = PlacementMode.Mouse;
500
501         //do *not* set the placement target, as it causes the popup to become hidden if the
502         //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through
503         //the ParentTaskbarIcon attached dependency property:
504         //tt.PlacementTarget = this;
505
506         //make sure the tooltip is invisible
507         tt.HasDropShadow = false;
508         tt.BorderThickness = new Thickness(0);
509         tt.Background = System.Windows.Media.Brushes.Transparent;
510
511         //setting the 
512         tt.StaysOpen = true;
513         tt.Content = TrayToolTip;
514       }
515       else if (tt == null && !String.IsNullOrEmpty(ToolTipText))
516       {
517         //create a simple tooltip for the string
518         tt = new ToolTip();
519         tt.Content = ToolTipText;
520       }
521
522       //the tooltip explicitly gets the DataContext of this instance.
523       //If there is no DataContext, the TaskbarIcon assigns itself
524       if (tt != null)
525       {
526         UpdateDataContext(tt, null, DataContext);
527       }
528
529       //store a reference to the used tooltip
530       SetTrayToolTipResolved(tt);
531     }
532
533
534     /// <summary>
535     /// Sets tooltip settings for the class depending on defined
536     /// dependency properties and OS support.
537     /// </summary>
538     private void WriteToolTipSettings()
539     {
540       const IconDataMembers flags = IconDataMembers.Tip;
541       iconData.ToolTipText = ToolTipText;
542
543       if (messageSink.Version == NotifyIconVersion.Vista)
544       {
545         //we need to set a tooltip text to get tooltip events from the
546         //taskbar icon
547         if (String.IsNullOrEmpty(iconData.ToolTipText) && TrayToolTipResolved != null)
548         {
549           //if we have not tooltip text but a custom tooltip, we
550           //need to set a dummy value (we're displaying the ToolTip control, not the string)
551           iconData.ToolTipText = "ToolTip";
552         }
553       }
554
555       //update the tooltip text
556       Util.WriteIconData(ref iconData, NotifyCommand.Modify, flags);
557     }
558
559     #endregion
560
561     #region Custom Popup
562
563     /// <summary>
564     /// Creates a <see cref="ToolTip"/> control that either
565     /// wraps the currently set <see cref="TrayToolTip"/>
566     /// control or the <see cref="ToolTipText"/> string.<br/>
567     /// If <see cref="TrayToolTip"/> itself is already
568     /// a <see cref="ToolTip"/> instance, it will be used directly.
569     /// </summary>
570     /// <remarks>We use a <see cref="ToolTip"/> rather than
571     /// <see cref="Popup"/> because there was no way to prevent a
572     /// popup from causing cyclic open/close commands if it was
573     /// placed under the mouse. ToolTip internally uses a Popup of
574     /// its own, but takes advance of Popup's internal <see cref="Popup.HitTestable"/>
575     /// property which prevents this issue.</remarks>
576     private void CreatePopup()
577     {
578       //check if the item itself is a popup
579       Popup popup = TrayPopup as Popup;
580
581       if (popup == null && TrayPopup != null)
582       {
583         //create an invisible popup that hosts the UIElement
584         popup = new Popup();
585         popup.AllowsTransparency = true;
586
587         //don't animate by default - devs can use attached
588         //events or override
589         popup.PopupAnimation = PopupAnimation.None;
590
591         //the CreateRootPopup method outputs binding errors in the debug window because
592         //it tries to bind to "Popup-specific" properties in case they are provided by the child.
593         //We don't need that so just assign the control as the child.
594         popup.Child = TrayPopup;
595
596         //do *not* set the placement target, as it causes the popup to become hidden if the
597         //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through
598         //the ParentTaskbarIcon attached dependency property:
599         //popup.PlacementTarget = this;
600
601         popup.Placement = PlacementMode.AbsolutePoint;
602         popup.StaysOpen = false;
603       }
604
605       //the popup explicitly gets the DataContext of this instance.
606       //If there is no DataContext, the TaskbarIcon assigns itself
607       if (popup != null)
608       {
609         UpdateDataContext(popup, null, DataContext);
610       }
611
612       //store a reference to the used tooltip
613       SetTrayPopupResolved(popup);
614     }
615
616
617     /// <summary>
618     /// Displays the <see cref="TrayPopup"/> control if
619     /// it was set.
620     /// </summary>
621     private void ShowTrayPopup(Point cursorPosition)
622     {
623       if (IsDisposed) return;
624
625       //raise preview event no matter whether popup is currently set
626       //or not (enables client to set it on demand)
627       var args = RaisePreviewTrayPopupOpenEvent();
628       if (args.Handled) return;
629
630       if (TrayPopup != null)
631       {
632         //use absolute position, but place the popup centered above the icon
633         TrayPopupResolved.Placement = PlacementMode.AbsolutePoint;
634         TrayPopupResolved.HorizontalOffset = cursorPosition.X;
635         TrayPopupResolved.VerticalOffset = cursorPosition.Y;
636
637         //open popup
638         TrayPopupResolved.IsOpen = true;
639
640
641         IntPtr handle = IntPtr.Zero;
642         if (TrayPopupResolved.Child != null)
643         {
644           //try to get a handle on the popup itself (via its child)
645           HwndSource source = (HwndSource)PresentationSource.FromVisual(TrayPopupResolved.Child);
646           if (source != null) handle = source.Handle;
647         }
648
649         //if we don't have a handle for the popup, fall back to the message sink
650         if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle;
651
652         //activate either popup or message sink to track deactivation.
653         //otherwise, the popup does not close if the user clicks somewhere else
654         WinApi.SetForegroundWindow(handle);
655
656         //raise attached event - item should never be null unless developers
657         //changed the CustomPopup directly...
658         if (TrayPopup != null) RaisePopupOpenedEvent(TrayPopup);
659
660         //bubble routed event
661         RaiseTrayPopupOpenEvent();
662       }
663     }
664
665     #endregion
666
667     #region Context Menu
668
669     /// <summary>
670     /// Displays the <see cref="ContextMenu"/> if
671     /// it was set.
672     /// </summary>
673     private void ShowContextMenu(Point cursorPosition)
674     {
675       if (IsDisposed) return;
676
677       //raise preview event no matter whether context menu is currently set
678       //or not (enables client to set it on demand)
679       var args = RaisePreviewTrayContextMenuOpenEvent();
680       if (args.Handled) return;
681
682       if (ContextMenu != null)
683       {
684         //use absolute position
685         ContextMenu.Placement = PlacementMode.AbsolutePoint;
686         ContextMenu.HorizontalOffset = cursorPosition.X;
687         ContextMenu.VerticalOffset = cursorPosition.Y;
688         ContextMenu.IsOpen = true;
689
690         //activate the message window to track deactivation - otherwise, the context menu
691         //does not close if the user clicks somewhere else
692         WinApi.SetForegroundWindow(messageSink.MessageWindowHandle);
693
694         //bubble event
695         RaiseTrayContextMenuOpenEvent();
696       }
697     }
698
699     #endregion
700
701     #region Balloon Tips
702
703     /// <summary>
704     /// Bubbles events if a balloon ToolTip was displayed
705     /// or removed.
706     /// </summary>
707     /// <param name="visible">Whether the ToolTip was just displayed
708     /// or removed.</param>
709     private void OnBalloonToolTipChanged(bool visible)
710     {
711       if (visible)
712       {
713         RaiseTrayBalloonTipShownEvent();
714       }
715       else
716       {
717         RaiseTrayBalloonTipClosedEvent();
718       }
719     }
720
721     /// <summary>
722     /// Displays a balloon tip with the specified title,
723     /// text, and icon in the taskbar for the specified time period.
724     /// </summary>
725     /// <param name="title">The title to display on the balloon tip.</param>
726     /// <param name="message">The text to display on the balloon tip.</param>
727     /// <param name="symbol">A symbol that indicates the severity.</param>
728     public void ShowBalloonTip(string title, string message, BalloonIcon symbol)
729     {
730       lock (this)
731       {
732         ShowBalloonTip(title, message, symbol.GetBalloonFlag(), IntPtr.Zero);
733       }
734     }
735
736
737     /// <summary>
738     /// Displays a balloon tip with the specified title,
739     /// text, and a custom icon in the taskbar for the specified time period.
740     /// </summary>
741     /// <param name="title">The title to display on the balloon tip.</param>
742     /// <param name="message">The text to display on the balloon tip.</param>
743     /// <param name="customIcon">A custom icon.</param>
744     /// <exception cref="ArgumentNullException">If <paramref name="customIcon"/>
745     /// is a null reference.</exception>
746     public void ShowBalloonTip(string title, string message, Icon customIcon)
747     {
748       if (customIcon == null) throw new ArgumentNullException("customIcon");
749
750       lock (this)
751       {
752         ShowBalloonTip(title, message, BalloonFlags.User, customIcon.Handle);
753       }
754     }
755
756
757     /// <summary>
758     /// Invokes <see cref="WinApi.Shell_NotifyIcon"/> in order to display
759     /// a given balloon ToolTip.
760     /// </summary>
761     /// <param name="title">The title to display on the balloon tip.</param>
762     /// <param name="message">The text to display on the balloon tip.</param>
763     /// <param name="flags">Indicates what icon to use.</param>
764     /// <param name="balloonIconHandle">A handle to a custom icon, if any, or
765     /// <see cref="IntPtr.Zero"/>.</param>
766     private void ShowBalloonTip(string title, string message, BalloonFlags flags, IntPtr balloonIconHandle)
767     {
768       EnsureNotDisposed();
769
770       iconData.BalloonText = message ?? String.Empty;
771       iconData.BalloonTitle = title ?? String.Empty;
772
773       iconData.BalloonFlags = flags;
774       iconData.CustomBalloonIconHandle = balloonIconHandle;
775       Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info | IconDataMembers.Icon);
776     }
777
778
779     /// <summary>
780     /// Hides a balloon ToolTip, if any is displayed.
781     /// </summary>
782     public void HideBalloonTip()
783     {
784       EnsureNotDisposed();
785
786       //reset balloon by just setting the info to an empty string
787       iconData.BalloonText = iconData.BalloonTitle = String.Empty;
788       Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info);
789     }
790
791     #endregion
792
793     #region Single Click Timer event
794
795     /// <summary>
796     /// Performs a delayed action if the user requested an action
797     /// based on a single click of the left mouse.<br/>
798     /// This method is invoked by the <see cref="singleClickTimer"/>.
799     /// </summary>
800     private void DoSingleClickAction(object state)
801     {
802       if (IsDisposed) return;
803
804       //run action
805       Action action = delayedTimerAction;
806       if (action != null)
807       {
808         //cleanup action
809         delayedTimerAction = null;
810
811         //switch to UI thread
812         this.GetDispatcher().Invoke(action);
813       }
814     }
815
816     #endregion
817
818     #region Set Version (API)
819
820     /// <summary>
821     /// Sets the version flag for the <see cref="iconData"/>.
822     /// </summary>
823     private void SetVersion()
824     {
825       iconData.VersionOrTimeout = (uint) NotifyIconVersion.Vista;
826       bool status = WinApi.Shell_NotifyIcon(NotifyCommand.SetVersion, ref iconData);
827
828       if (!status)
829       {
830         iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win2000;
831         status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion);
832       }
833
834       if (!status)
835       {
836         iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win95;
837         status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion);
838       }
839
840       if (!status)
841       {
842         Debug.Fail("Could not set version");
843       }
844     }
845
846     #endregion
847
848     #region Create / Remove Taskbar Icon
849
850     /// <summary>
851     /// Recreates the taskbar icon if the whole taskbar was
852     /// recreated (e.g. because Explorer was shut down).
853     /// </summary>
854     private void OnTaskbarCreated()
855     {
856       IsTaskbarIconCreated = false;
857       CreateTaskbarIcon();
858     }
859
860
861     /// <summary>
862     /// Creates the taskbar icon. This message is invoked during initialization,
863     /// if the taskbar is restarted, and whenever the icon is displayed.
864     /// </summary>
865     private void CreateTaskbarIcon()
866     {
867       lock (this)
868       {
869         if (!IsTaskbarIconCreated)
870         {
871           const IconDataMembers members = IconDataMembers.Message
872                                           | IconDataMembers.Icon
873                                           | IconDataMembers.Tip;
874
875           //write initial configuration
876           var status = Util.WriteIconData(ref iconData, NotifyCommand.Add, members);
877           if (!status)
878           {
879             throw new Win32Exception("Could not create icon data");
880           }
881
882           //set to most recent version
883           SetVersion();
884           messageSink.Version = (NotifyIconVersion) iconData.VersionOrTimeout;
885
886           IsTaskbarIconCreated = true;
887         }
888       }
889     }
890
891
892     /// <summary>
893     /// Closes the taskbar icon if required.
894     /// </summary>
895     private void RemoveTaskbarIcon()
896     {
897       lock (this)
898       {
899         if (IsTaskbarIconCreated)
900         {
901           Util.WriteIconData(ref iconData, NotifyCommand.Delete, IconDataMembers.Message);
902           IsTaskbarIconCreated = false;
903         }
904       }
905     }
906
907     #endregion
908
909     #region Dispose / Exit
910
911     /// <summary>
912     /// Set to true as soon as <see cref="Dispose"/>
913     /// has been invoked.
914     /// </summary>
915     public bool IsDisposed { get; private set; }
916
917
918     /// <summary>
919     /// Checks if the object has been disposed and
920     /// raises a <see cref="ObjectDisposedException"/> in case
921     /// the <see cref="IsDisposed"/> flag is true.
922     /// </summary>
923     private void EnsureNotDisposed()
924     {
925       if (IsDisposed) throw new ObjectDisposedException(Name ?? GetType().FullName);
926     }
927
928
929     /// <summary>
930     /// Disposes the class if the application exits.
931     /// </summary>
932     private void OnExit(object sender, EventArgs e)
933     {
934       Dispose();
935     }
936
937
938     /// <summary>
939     /// This destructor will run only if the <see cref="Dispose()"/>
940     /// method does not get called. This gives this base class the
941     /// opportunity to finalize.
942     /// <para>
943     /// Important: Do not provide destructors in types derived from
944     /// this class.
945     /// </para>
946     /// </summary>
947     ~TaskbarIcon()
948     {
949       Dispose(false);
950     }
951
952
953     /// <summary>
954     /// Disposes the object.
955     /// </summary>
956     /// <remarks>This method is not virtual by design. Derived classes
957     /// should override <see cref="Dispose(bool)"/>.
958     /// </remarks>
959     public void Dispose()
960     {
961       Dispose(true);
962
963       // This object will be cleaned up by the Dispose method.
964       // Therefore, you should call GC.SupressFinalize to
965       // take this object off the finalization queue 
966       // and prevent finalization code for this object
967       // from executing a second time.
968       GC.SuppressFinalize(this);
969     }
970
971
972     /// <summary>
973     /// Closes the tray and releases all resources.
974     /// </summary>
975     /// <summary>
976     /// <c>Dispose(bool disposing)</c> executes in two distinct scenarios.
977     /// If disposing equals <c>true</c>, the method has been called directly
978     /// or indirectly by a user's code. Managed and unmanaged resources
979     /// can be disposed.
980     /// </summary>
981     /// <param name="disposing">If disposing equals <c>false</c>, the method
982     /// has been called by the runtime from inside the finalizer and you
983     /// should not reference other objects. Only unmanaged resources can
984     /// be disposed.</param>
985     /// <remarks>Check the <see cref="IsDisposed"/> property to determine whether
986     /// the method has already been called.</remarks>
987     private void Dispose(bool disposing)
988     {
989       //don't do anything if the component is already disposed
990       if (IsDisposed || !disposing) return;
991
992       lock (this)
993       {
994         IsDisposed = true;
995
996         //deregister application event listener
997         if (Application.Current != null)
998         {
999           Application.Current.Exit -= OnExit;
1000         }
1001
1002         //stop timers
1003         singleClickTimer.Dispose();
1004         balloonCloseTimer.Dispose();
1005
1006         //dispose message sink
1007         messageSink.Dispose();
1008
1009         //remove icon
1010         RemoveTaskbarIcon();
1011       }
1012     }
1013
1014     #endregion
1015   }
1016 }