Showing posts with label Mouse Gestures. Show all posts
Showing posts with label Mouse Gestures. Show all posts

Monday, August 31, 2009

A Pattern For Control-Specific Context Menu on Right-Click in Silverlight

There are a lot of good examples online, about how to make Silverlight applications respond to mouse right-clicks. So, I am not going to rehash it in detail here. Instead, I will attempt to describe a pattern that I found useful, when I implemented right-click context menus for controls on a page. The requirements for the functionality are:

1. There is a unified and consistent way to display context menu.
2. The control provides the data for the display (image / text / data).
3. The control is responsible for responding to the user selection.

Here is a demo application that uses the pattern I describe. Just as before, there is something about the script on the silverlight live site that doesn't make it work right for firefox. Works fine in IE, and when hosted on another site, on Firefox too. Since the app works ok when code is compiled and run on any browser, I am not going to sweat it.



Link to app here: http://silverlight.services.live.com/invoke/105917/Control-Specific%20Right-click%20Context%20Menu%20Demo/iframe.html

The codebase can be downloaded here: http://www.filefactory.com/file/ah498ca/n/ContextMenuDemo_zip


The main component of the functionality is the right-click interceptor class, which interacts with the DOM and intercepts the right mouse down, mouseclick, oncontextmenu events. Make sure your app is set as "windowless". All these aspects, it shares with other examples online. We will come to some extra functionality of this class later. The RootVisual of the app has an instance of this class.

  1         private void OnMouseDown(object sender, HtmlEventArgs e)
  2         {
  3             if (e.MouseButton == MouseButtons.Right)
  4             {
  5                 foreach (FrameworkElement ei in _ElementsToIntercept)
  6                 {
  7                     if (IsPointWithinBounds(new Point(e.OffsetX, e.OffsetY), ei))
  8                     {
  9                         e.PreventDefault();
 10                         if (MouseRightButtonDown != null)
 11                             MouseRightButtonDown(this, e);
 12                         RightClickedControl = ei;
 13                         _IsButtonDown = true;
 14                         break;
 15                     }
 16                 }
 17             }
 18         }
 19         private void OnMouseUp(object sender, HtmlEventArgs e)
 20         {
 21             if (e.MouseButton == MouseButtons.Right && _IsButtonDown)
 22             {
 23                 e.PreventDefault();
 24                 _IsButtonDown = false;
 25                 if (MouseRightButtonUp != null)
 26                     MouseRightButtonUp(this, e);
 27             }
 28         }
 29 
 30         private void OnContextMenu(object sender, HtmlEventArgs e)
 31         {
 32             foreach (FrameworkElement ei in _ElementsToIntercept)
 33             {
 34                 if (IsPointWithinBounds(new Point(e.OffsetX, e.OffsetY), ei))
 35                 {
 36                     e.PreventDefault();
 37                     break;
 38                 }
 39             }
 40         }
 41 


The second component is the "ISupportCOntextMenu" interface.

  1     public interface ISupportContextMenu
  2     {
  3         void HandleContextMenuClick(object sender, ContextMenuClickEventArgs e);
  4         List<ContextMenuItemInfo> GetContextMenuContent(HtmlEventArgs e);
  5     }

Any control that needs a context menu needs to implement this interface, and the control needs to be registered with the rootvisual. Thre registration process brings us to the extra stuff in the interceptor. Interceptor maintains a list of references to ISupportContextMenu. Registration just adds to this list. When an interceptable event happens, the interceptor checks to see if it happened within the bounds of any of the items in the list (Big part of the whole thing).

  1         public static bool IsPointWithinBounds(Point p, FrameworkElement element)
  2         {
  3             try
  4             {
  5                 GeneralTransform gt = element.TransformToVisual(Application.Current.RootVisual as FrameworkElement);
  6                 Point cp = gt.Transform(new Point(0, 0));
  7 
  8                 if (p.X <= cp.X + element.ActualWidth && p.X >= cp.X && p.Y <= cp.Y + element.ActualHeight && p.Y >= cp.Y)
  9                     return true;
 10                 else
 11                     return false;
 12             }
 13             //if the control has been cleared from parent 'transformtovisual' will throw an error.
 14             //In that case return false
 15             catch { return false; }
 16         }

If it did, then it asks the control to provide data for the context menu (GetContextMenuContent()).

  1 //From instantiating control:
  2 
  3         public Home()
  4         {
  5             InitializeComponent();
  6             ((MainPage)App.Current.RootVisual).RegisterControlWithContextMenu(Ctx1);
  7             ((MainPage)App.Current.RootVisual).RegisterControlWithContextMenu(Ctx2);
  8         }
  9 
 10 //From RootVisual:
 11 
 12         public void RegisterControlWithContextMenu(ISupportContextMenu control)
 13         {
 14             _RightClickIntrcpt.AddElementToIntercept(control);
 15         }
 16 
 17 //From Interceptor:
 18 
 19         public void AddElementToIntercept(ISupportContextMenu fe)
 20         {
 21             if (!_ElementsToIntercept.Contains(fe))
 22                 _ElementsToIntercept.Add(fe);
 23         }


This brings us to the third component: ContextMenuItemInfo.

  1     public class ContextMenuItemInfo
  2     {
  3         public Image Image { get; set; }
  4         public string Command { get; set; }
  5         public Object Tag { get; set; }
  6     }
You can provide a list of items, each of which contains an image (optional), text, and any kind of data that makes sense in the context (optional) to the interceptor. The text and data tag is passed back to the control as a KeyValuePair if user click on it.

The third component is a popup control that holds a list box. This is in the root visual. The RootVisual responds to an event raised by the interceptor and takes the  List of ContextMenuItemInfo, populates the listbox, moves the popup to where the mouse is, and displays it. There is a DispatcherTimer and MouseEnter/MouseLeave events that make sure the popup is displayed and hidden in a graceful manner. In the RootVisual:

  1         private void InitializeContextMenuIntercept()
  2         {
  3             InitializeContextMenuTimer();
  4             _RightClickIntrcpt = new ContextMenuInterceptor();
  5             _RightClickIntrcpt.MouseRightButtonUp += new EventHandler<System.Windows.Browser.HtmlEventArgs>(_RightClickIntrcpt_MouseRightButtonUp);
  6         }
  7 
  8         private void InitializeContextMenuTimer()
  9         {
 10             _ContextMenuTimer = new DispatcherTimer();
 11             _ContextMenuTimer.Interval = TimeSpan.FromSeconds(2);
 12             _ContextMenuTimer.Tick += (s, e) => ContextMenu_MouseLeave(null, null);
 13         }
 14 
 15         void _RightClickIntrcpt_MouseRightButtonUp(object sender, System.Windows.Browser.HtmlEventArgs e)
 16         {
 17             ISupportContextMenu cntrl = _RightClickIntrcpt.RightClickedControl as ISupportContextMenu;
 18             if (cntrl != null)
 19             {
 20                 List<ContextMenuItemInfo> mnuContent = cntrl.GetContextMenuContent(e);
 21                 if (mnuContent != null && mnuContent.Count > 0)
 22                 {
 23                     ContextMenuListBox.ItemsSource = from mc in mnuContent
 24                                                      select new ListBoxItem
 25                                                      {
 26                                                          Content = GetContextMenuItemVisual(mc.Command, mc.Image),
 27                                                          Tag = new KeyValuePair<string, object>(mc.Command, mc.Tag)
 28                                                      };
 29                     ContextMenuListBox.MouseLeftButtonUp += ContextMenuListBox_MouseLeftButtonUp;
 30                     ContextMenuPopup.Margin = new Thickness(e.OffsetX, e.OffsetY, 0, 0);
 31                     ContextMenuPopup.IsOpen = true;
 32                     _ContextMenuTimer.Start();
 33                 }
 34             }
 35         }
 36 
 37         FrameworkElement GetContextMenuItemVisual(string command, Image image)
 38         {
 39             StackPanel ret = new StackPanel
 40             {
 41                 Orientation = Orientation.Horizontal,
 42                 Margin = new Thickness(2),
 43             };
 44             if (image != null)
 45             {
 46                 image.Margin = new Thickness(0, 0, 4, 0);
 47                 ret.Children.Add(image);
 48             }
 49             ret.Children.Add(new TextBlock { Text = command });
 50             return ret;
 51         }
 52 
 53 
 54         void ContextMenuListBox_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
 55         {
 56             if (ContextMenuListBox.SelectedItem != null)
 57                 ((ISupportContextMenu)_RightClickIntrcpt.RightClickedControl).HandleContextMenuClick(this, new ContextMenuClickEventArgs
 58                 {
 59                     ClickedItem = (KeyValuePair<string, object>)((ListBoxItem)ContextMenuListBox.SelectedItem).Tag
 60                 });
 61             ContextMenuListBox.MouseLeftButtonUp -= ContextMenuListBox_MouseLeftButtonUp;
 62             ContextMenuPopup.IsOpen = false;
 63         }
 64 
 65         void ContextMenu_MouseEnter(object sender, RoutedEventArgs e)
 66         {
 67             _ContextMenuTimer.Stop();
 68             ContextMenuPopup.IsOpen = true;
 69         }
 70         void ContextMenu_MouseLeave(object sender, RoutedEventArgs e)
 71         {
 72             ContextMenuPopup.IsOpen = false;
 73             _ContextMenuTimer.Stop();
 74         }



This ensures that a control has total say if it wants a context menu, and if so, what the content should be, and how the application should respond to user selection. I know that a lot of things are not elaborated here, but I am hoping it will make sense when you go through the code. If needed, I can augment this with some diagrams. Let me know.

Monday, August 24, 2009

Mouse DoubleClick in Silverlight

We are implementing a mouse-doubleclick event in the TimeLine Visualizer control, but that is not native to Silverlight. There are a number of code snippets on the web about how to do this, but I think the implementation of the event is nice enough to share here.

The event fires when the mouse is clicked twice within a certain time, and within a certain distance. I think the spatial/temporal tolerances make for a smooth user experience. The code consists of static variables to store the position and time of last mouse-click, and extension methods to compare the current values to the stored values. The code should be self-explanatory (While we are one the subject, can anybody point me to a good online code formatter for blogger? Live Writer refuses to install on my machine which has Windows 7 Ultimate, 32-bit):

1:      #region Mouse Double Click  
2: //initialize to 2 secs back, so event doesnt fire very first time user clicks on control
3: static DateTime _lastClicked = DateTime.Now.AddSeconds(-2);
4: static Point _lastMousePosition = new Point(1, 1);
5: private const double MousePositionTolerance = 20;
6: public static bool IsDoubleClick(this MouseButtonEventArgs e)
7: {
8: bool ret = false;
9: if ((DateTime.Now.Subtract(_lastClicked) < TimeSpan.FromMilliseconds(500))
10: && (e.GetPosition(null).GetDistance(_lastMousePosition) < MousePositionTolerance))
11: {
12: ret = true;
13: }
14: _lastClicked = DateTime.Now;
15: _lastMousePosition = e.GetPosition(null);
16: return ret;
17: }
18: #endregion
19: public static double GetDistance(this Point current, Point other)
20: {
21: double x = current.X - other.X;
22: double y = current.Y - other.Y;
23: return Math.Sqrt(x * x + y * y);
24: }


The consumer code will handle the MouseLeftButtonDown event and examine it to see if it is a double-click, like this:

private void HandleMouseLeftButtonDown(object sender, MouseButtonEventArgs e)

{

if (e.IsDoubleClick())

{
....

Caveat Emptor:
The calling method should make sure that all code paths execute e.DoubleClick().

Take, for instance, a calling method like this:

if (condition1 == true)
{
condition1 = false;
do something;
}
else if (e.IsDoubleClick())
{
Do what you do on doubleclick;
}

Here IsDoubleClick is not called the first time the Mousedown event happens, so the static variables _lastPosition and _lastClicked are not set. Hence, IsDoubleClick will not return true when a true double-click happens.

Hope this helps.