vbAccelerator - Contents of code file: CancellableEditPopupCS_PopupCancelNotifier.cs

using System;
using System.Drawing;
using System.Windows.Forms;

namespace vbAccelerator.Components.Controls
{
   /// <summary>
   /// Represents the method that handles the <see
    cref="PopupCancelNotifier.PopupCancel"/> event
   /// raised by this class.
   /// </summary>
   public delegate void PopupCancelEventHandler(object sender,
    PopupCancelEventArgs e);

   /// <summary>
   /// Arguments to a <see cref="PopupCancelEvent"/>.  Provides a
   /// reference to the popup form that is to be closed and 
   /// allows the operation to be cancelled.
   /// </summary>
   public class PopupCancelEventArgs : EventArgs
   {
      /// <summary>
      /// Whether to cancel the operation
      /// </summary>
      private bool cancel = false;
      /// <summary>
      /// Mouse down location
      /// </summary>
      private Point location;
      /// <summary>
      /// Popup control.
      /// </summary>
      private Control popup = null;

      /// <summary>
      /// Constructs a new instance of this class.
      /// </summary>
      /// <param name="popup">The popup form</param>
      /// <param name="location">The mouse location, if any, where the
      /// mouse event that would cancel the popup occured.</param>
      public PopupCancelEventArgs(Control popup, Point location)
      {
         this.popup = popup;
         this.location = location;
         this.cancel = false;
      }

      /// <summary>
      /// Gets the popup control
      /// </summary>
      public Control Popup
      {
         get
         {
            return this.popup;
         }
      }

      /// <summary>
      /// Gets the location that the mouse down which would cancel this 
      /// popup occurred
      /// </summary>
      public Point CursorLocation
      {
         get
         {
            return this.location;
         }
      }

      /// <summary>
      /// Gets/sets whether to cancel closing the form. Set to
      /// <c>true</c> to prevent the popup from being closed.
      /// </summary>
      public bool Cancel
      {
         get
         {
            return this.cancel;
         }
         set
         {
            this.cancel = value;
         }
      }

   }


   /// <summary>
   /// 
   /// A class which provides the functionality required to 
   /// cancel a popup window.  This class wraps two pieces of 
   /// functionality:
   /// 
   /// <list type="number">Firstly, it checks whether the form (or the form
    owner
   /// for the control) receives a <c>WM_APPACTIVATE</c> message with
   /// wParam = 0.  This indicates the window has gone out
   /// of focus because the user has clicked on another one.</list>
   /// <list type="number">Secondly, it installs a <see
    cref="System.Windows.Forms.IMessageFilter"/>
   /// message filter implementation which checks for mouse presses anywhere 
   /// else in the application.
   /// 
   /// <remarks>
   /// Copyright &#169; 2003 Steve McMahon for vbAccelerator.com.
   /// vbAccelerator is a Trade Mark of vbAccelerator Ltd.  All Rights
   /// Reserved.  Please visit http://vbaccelerator.com/ for more
   /// on this and other VB and .NET Framework code.
   /// </remarks>
   /// 
   /// </summary>
   public class PopupCancelNotifier : NativeWindow
   {
      private const int WM_ACTIVATEAPP = 0x01C;

      /// <summary>
      /// Raised when the popup control is about to be cancelled.
      /// </summary>
      public event PopupCancelEventHandler PopupCancel;

      /// <summary>
      /// The <see cref="System.Windows.Forms.IMessageFilter"/> object
      /// which checks for mouse down outside the control
      /// </summary>
      private PopupCancelNotifierMessageFilter filter = null;       

      /// <summary>
      /// Owning Form's Window handle to track for popup cancellation
      /// </summary>
      private IntPtr trackHandle = IntPtr.Zero;

      /// <summary>
      /// Control to track for popup cancellation
      /// </summary>
      private Control trackControl = null;

      /// <summary>
      /// Start tracking for a popup cancellation.
      /// </summary>
      /// <param name="ctl">The <c>Control</c> or <c>Form</c>
      /// to use when tracking Window inactivation messages. This can
      /// either be a control or a Form.</param>
      public void StartTracking(Control ctl)
      {
         IntPtr handle = IntPtr.Zero;

         Control ctlOwnerForm = ctl;
         Control ctlTest = null;
         while (!typeof(Form).IsAssignableFrom(ctlOwnerForm.GetType()))
         {
            ctlTest = ctlOwnerForm.Parent;
            if (ctlTest == null)
            {
               break;
            }
            else
            {
               ctlOwnerForm = ctlTest;
            }
         }
                  
         this.trackControl = ctl;
         filter.Popup = ctl;
         this.trackHandle = ctlOwnerForm.Handle;
         this.AssignHandle(trackHandle);
         Application.AddMessageFilter(filter);         
         
      }

      /// <summary>
      /// Check for the WM_APPACTIVATE message and stop
      /// tracking if the window is inactivated.
      /// </summary>
      /// <param name="msg">Message details for this window procedure
      /// event.</param>
      protected override void WndProc(ref Message msg)
      {
         base.WndProc(ref msg);
         if (msg.Msg == WM_ACTIVATEAPP)
         {
            if (((int)msg.WParam) == 0)
            {
               PopupCancelEventArgs e = new PopupCancelEventArgs(
                  this.trackControl, Cursor.Position);
               OnPopupCancel(e);
            }
         }
      }

      /// <summary>
      /// Stop tracking. Called automatically if this class determines
      /// the popup should be cancelled.
      /// </summary>
      public void StopTracking()
      {
         if (!this.trackHandle.Equals(IntPtr.Zero))
         {
            this.ReleaseHandle();
            this.trackHandle = IntPtr.Zero;
            Application.RemoveMessageFilter(filter);
            filter.Popup = null;
         }
      }

      /// <summary>
      /// Pass through for the PopupCancel event of the Message Filter
      /// </summary>
      /// <param name="sender">The <see
       cref="PopupCancelNotifierEventFilter"/></param>
      /// <param name="e"><see cref="PopupCancelEventArgs"/> describing the
       event
      /// that will cancel the popup.</param>
      private void filter_PopupCancel(object sender, PopupCancelEventArgs e)
      {
         OnPopupCancel(e);
      }

      /// <summary>
      /// Notify when the popup should be cancelled,
      /// and uninstall tracking.
      /// </summary>
      protected virtual void OnPopupCancel(PopupCancelEventArgs e)
      {
         if (PopupCancel != null)
         {
            PopupCancel(this, e);
         }
         if (!e.Cancel)
         {
            StopTracking();
         }
      }

      /// <summary>
      /// Constructs a new instance of the PopupCancelNotifier
      /// class.
      /// </summary>
      public PopupCancelNotifier() : base()
      {
         this.filter = new PopupCancelNotifierMessageFilter(this);
         this.filter.PopupCancel += new
          PopupCancelEventHandler(filter_PopupCancel);
      }
   }

   #region PopupWindowHelperMessageFilter
   /// <summary>
   /// A Message Loop filter which detect mouse events whilst the popup form is
    shown
   /// and notifies the owning <see cref="PopupWindowHelper"/> class when a
    mouse
   /// click outside the popup occurs.
   /// </summary>
   public class PopupCancelNotifierMessageFilter : IMessageFilter
   {
      private const int WM_LBUTTONDOWN = 0x201;
      private const int WM_RBUTTONDOWN = 0x204;
      private const int WM_MBUTTONDOWN = 0x207;
      private const int WM_NCLBUTTONDOWN = 0x0A1;
      private const int WM_NCRBUTTONDOWN = 0x0A4;
      private const int WM_NCMBUTTONDOWN = 0x0A7;

      /// <summary>
      /// Raised when the Popup COntrol is about to be cancelled.  The
      /// <see cref="PopupCancelEventArgs.Cancel"/> property can be
      /// set to <c>true</c> to prevent the control from being cancelled.
      /// </summary>
      public event PopupCancelEventHandler PopupCancel;
      
      /// <summary>
      /// The popup control
      /// </summary>
      private Control popup = null;
      /// <summary>
      /// The owning <see cref="PopupCancelNotifier"/> object.
      /// </summary>
      private PopupCancelNotifier owner = null;

      /// <summary>
      /// Constructs a new instance of this class and sets the owning
      /// object.
      /// </summary>
      /// <param name="owner">The <see cref="PopupCancelNotifier"/> object
      /// which owns this class.</param>
      public PopupCancelNotifierMessageFilter(PopupCancelNotifier owner)
      {
         this.owner = owner;
      }

      /// <summary>
      /// Gets/sets the popup <see cref="System.Windows.Forms.Control"/> which
       is being displayed.
      /// </summary>
      public Control Popup
      {
         get
         {
            return this.popup;
         }
         set
         {
            this.popup = value;
         }
      }

      /// <summary>
      /// Checks the message loop for mouse messages whilst the popup
      /// window is displayed.  If one is detected the position is
      /// checked to see if it is outside the form, and the owner
      /// is notified if so.
      /// </summary>
      /// <param name="m">Windows Message about to be processed by the
      /// message loop</param>
      /// <returns><c>true</c> to filter the message, <c>false</c> otherwise.
      /// This implementation always returns <c>false</c>.</returns>
      public bool PreFilterMessage(ref Message m)
      {
         if (this.popup != null)
         {
            switch (m.Msg)
            {            
               case WM_LBUTTONDOWN:
               case WM_RBUTTONDOWN:
               case WM_MBUTTONDOWN:
               case WM_NCLBUTTONDOWN:
               case WM_NCRBUTTONDOWN:
               case WM_NCMBUTTONDOWN:
                  OnMouseDown();
                  break;
            }
         }
         return false;
      }

      /// <summary>
      /// Checks the mouse location and calls the OnCancelPopup method
      /// if the mouse is outside the popup form.      
      /// </summary>
      private void OnMouseDown()
      {
         Console.WriteLine("Filter:OnMouseDown");
         if (this.popup != null)
         {
            // Get the cursor location
            Point cursorPos = Cursor.Position;
            // To control coordinates:
            cursorPos = this.popup.PointToClient(cursorPos);
            // Check if it is within the popup control
            if (!popup.ClientRectangle.Contains(cursorPos))
            {
               // If not, then call to see if it should be closed               
               OnCancelPopup(new PopupCancelEventArgs(popup, cursorPos));
            }
         }
      }

      /// <summary>
      /// Raises the <see cref="PopupCancel"/> event.
      /// </summary>
      /// <param name="e">The <see cref="PopupCancelEventArgs"/> associated 
      /// with the cancel event.</param>
      protected virtual void OnCancelPopup(PopupCancelEventArgs e)
      {
         if (this.PopupCancel != null)
         {
            this.PopupCancel(this, e);
         }
         if (!e.Cancel)
         {
            owner.StopTracking();
            // Clear reference for GC
            popup = null;
         }
      }


   }
   #endregion

}