Statistics
| Branch: | Revision:

root / trunk / NotifyIconWpf / TaskbarIcon.cs @ 9bae55d1

History | View | Annotate | Download (31 kB)

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
}