Submenu hysteresis patch



I've taken Nils Barth's excellent submenu navigation patch and performed
some manipulations in an attempt to finish/clean it up. 

* Created a padding area for the navigation region, so that the region is
much easier to get into when navigating into a submenu that is not to the
right and below the current menu item. 
* Added a timeout of about 1/3rd of a second. Easily changable with a
#define. 
* Moved all the code into gtkmenu.c (and it works there quite naturally, I
think). This involved adding enter/leave_notify handlers (I tried
gtk_grab_Add() but could not get it to work). 

Please let me know what you think and what changes have to be made.
Thanks, 
   David :)
Index: gtkmenu.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenu.c,v
retrieving revision 1.46
diff -u -r1.46 gtkmenu.c
--- gtkmenu.c	2000/06/20 21:04:38	1.46
+++ gtkmenu.c	2000/07/05 22:20:47
@@ -34,10 +34,12 @@
 #include "gtksignal.h"
 #include "gtkwindow.h"
 
-
 #define MENU_ITEM_CLASS(w)   GTK_MENU_ITEM_GET_CLASS (w)
 #define	MENU_NEEDS_RESIZE(m) GTK_MENU_SHELL (m)->menu_flag
 
+#define SUBMENU_NAV_REGION_PADDING 2
+#define SUBMENU_NAV_HYSTERESIS_TIMEOUT 333
+
 typedef struct _GtkMenuAttachData	GtkMenuAttachData;
 
 struct _GtkMenuAttachData
@@ -64,6 +66,16 @@
 				     GdkEventKey       *event);
 static gint gtk_menu_motion_notify  (GtkWidget	       *widget,
 				     GdkEventMotion    *event);
+static gint gtk_menu_enter_notify   (GtkWidget         *widget,
+				     GdkEventCrossing  *event); 
+static gint gtk_menu_leave_notify   (GtkWidget         *widget,
+				     GdkEventCrossing  *event);
+static void gtk_menu_stop_navigating_submenu (void);
+static gint gtk_menu_stop_navigating_submenu_cb (gpointer user_data); 
+static gboolean gtk_menu_navigating_submenu (gint event_x, 
+					     gint event_y);
+static void gtk_menu_set_submenu_navigation_region (GtkMenuItem   *menu_item, 
+						    GdkEventCrossing *event); 
 static void gtk_menu_deactivate	    (GtkMenuShell      *menu_shell);
 static void gtk_menu_show_all       (GtkWidget         *widget);
 static void gtk_menu_hide_all       (GtkWidget         *widget);
@@ -75,8 +87,8 @@
 static GtkMenuShellClass *parent_class = NULL;
 static const gchar	 *attach_data_key = "gtk-menu-attach-data";
 static GQuark             quark_uline_accel_group = 0;
+static GdkRegion         *navigation_region = NULL; 
 
-
 GtkType
 gtk_menu_get_type (void)
 {
@@ -129,6 +141,8 @@
   widget_class->motion_notify_event = gtk_menu_motion_notify;
   widget_class->show_all = gtk_menu_show_all;
   widget_class->hide_all = gtk_menu_hide_all;
+  widget_class->enter_notify_event = gtk_menu_enter_notify;
+  widget_class->leave_notify_event = gtk_menu_leave_notify;
   
   menu_shell_class->submenu_placement = GTK_LEFT_RIGHT;
   menu_shell_class->deactivate = gtk_menu_deactivate;
@@ -1029,6 +1043,8 @@
       
   menu_shell = GTK_MENU_SHELL (widget);
 
+  gtk_menu_stop_navigating_submenu (); 
+
   if (GTK_WIDGET_CLASS (parent_class)->key_press_event (widget, event))
     return TRUE;
 
@@ -1106,11 +1122,21 @@
 gtk_menu_motion_notify  (GtkWidget	   *widget,
 			 GdkEventMotion    *event)
 {
+  GtkMenu *menu;
+  GtkMenuShell *menu_shell; 
+
   g_return_val_if_fail (widget != NULL, FALSE);
   g_return_val_if_fail (GTK_IS_MENU (widget), FALSE);
   
-  if (GTK_MENU_SHELL (widget)->ignore_enter)
-    GTK_MENU_SHELL (widget)->ignore_enter = FALSE;
+  menu = GTK_MENU (widget); 
+
+  if (gtk_menu_navigating_submenu (event->x_root, event->y_root))
+    return TRUE; 
+
+  menu_shell = GTK_MENU_SHELL (widget); 
+  
+  if (menu_shell->ignore_enter)
+    menu_shell->ignore_enter = FALSE; 
   else 
     {
       gint width, height;
@@ -1131,6 +1157,225 @@
     }
 
   return FALSE;
+}
+
+static gint
+gtk_menu_enter_notify (GtkWidget        *widget,
+		       GdkEventCrossing *event)
+{
+  GtkMenu *menu;
+  GtkWidget *event_widget;
+  
+  g_return_val_if_fail (widget != NULL, FALSE);
+  g_return_val_if_fail (GTK_IS_MENU_SHELL (widget), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  menu = GTK_MENU (widget);
+
+  if (GTK_MENU_SHELL (menu)->active && !GTK_MENU_SHELL (menu)->ignore_enter)
+    {
+      event_widget = gtk_get_event_widget ((GdkEvent*) event); 
+
+      if (!event_widget || !GTK_WIDGET_IS_SENSITIVE (event_widget))
+	return TRUE;
+
+      if (gtk_menu_navigating_submenu (event->x_root, event->y_root))
+	return TRUE; /* Don't do anything -- we're navigating */ 
+      
+      if ((event_widget->parent == widget) &&
+	  (GTK_MENU_SHELL (menu)->active_menu_item != event_widget) &&
+	  GTK_IS_MENU_ITEM (event_widget))
+	{
+	  if ((event->detail != GDK_NOTIFY_INFERIOR) &&
+	      (GTK_WIDGET_STATE (event_widget) != GTK_STATE_PRELIGHT))
+	    {
+	      gtk_menu_shell_select_item (GTK_MENU_SHELL (menu), event_widget);
+	    }
+	}
+      else if (GTK_MENU_SHELL (menu)->parent_menu_shell)
+	{
+	  gtk_widget_event (GTK_MENU_SHELL (menu)->parent_menu_shell, (GdkEvent*) event);
+	}
+    }
+
+  return TRUE;
+}
+
+static gint
+gtk_menu_leave_notify (GtkWidget        *widget,
+		       GdkEventCrossing *event)
+{
+  GtkMenu *menu;
+  GtkMenuItem *menu_item;
+  GtkWidget *event_widget;
+
+  g_return_val_if_fail (widget != NULL, FALSE);
+  g_return_val_if_fail (GTK_IS_MENU_SHELL (widget), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  if (GTK_WIDGET_VISIBLE (widget))
+    {
+      menu = GTK_MENU (widget);
+      event_widget = gtk_get_event_widget ((GdkEvent*) event);
+
+      if (!event_widget || !GTK_IS_MENU_ITEM (event_widget))
+	return TRUE;
+
+      menu_item = GTK_MENU_ITEM (event_widget);
+
+      if (GTK_MENU_SHELL (menu)->ignore_leave)
+	{
+	  GTK_MENU_SHELL (menu)->ignore_leave = FALSE;
+	  return TRUE;
+	}
+
+      if (!GTK_WIDGET_IS_SENSITIVE (menu_item))
+	return TRUE;
+
+      if (gtk_menu_navigating_submenu (event->x_root, event->y_root))
+	return TRUE; /* Don't do anything -- we're navigating */
+
+      /* Here we check to see if we're leaving an active menu item with a submenu, 
+       * in which case we enter submenu navigation mode. 
+       */
+      if (GTK_MENU_SHELL (menu)->active_menu_item != NULL
+	  && menu_item->submenu != NULL
+	  && menu_item->submenu_placement == GTK_LEFT_RIGHT)
+	{
+	  if (menu_item->submenu->window != NULL) 
+	    {
+	      gtk_menu_set_submenu_navigation_region (menu_item, event);
+	      return TRUE;
+	    }
+	}
+
+      if ((GTK_MENU_SHELL (menu)->active_menu_item == event_widget) &&
+	  (menu_item->submenu == NULL))
+	{
+	  if ((event->detail != GDK_NOTIFY_INFERIOR) &&
+	      (GTK_WIDGET_STATE (menu_item) != GTK_STATE_NORMAL))
+	    {
+	      gtk_menu_shell_deselect (GTK_MENU_SHELL (menu));
+	    }
+	}
+      else if (GTK_MENU_SHELL (menu)->parent_menu_shell)
+	{
+	  gtk_widget_event (GTK_MENU_SHELL (menu)->parent_menu_shell, (GdkEvent*) event);
+	}
+    }
+ 
+    return TRUE;
+}
+
+static void 
+gtk_menu_stop_navigating_submenu (void)
+{
+  if (navigation_region) 
+    {
+      gdk_region_destroy (navigation_region);
+      navigation_region = NULL;
+    }
+}
+
+static gint
+gtk_menu_stop_navigating_submenu_cb (gpointer user_data)
+{
+  GdkEvent send_event; 
+  GdkWindow *window_at_pointer; 
+
+  gtk_menu_stop_navigating_submenu (); 
+
+  /* Here we get the mouse pointer and send an enter event
+     to the window currently under the mouse. That way, if the
+     user is navigating a submenu, and the timeout runs out, 
+     the menu item under his mouse is selected without him having
+     to move the mouse one more time for a new motion_notify event. */ 
+  window_at_pointer = gdk_window_at_pointer (NULL, NULL);
+
+  send_event.crossing.type = GDK_ENTER_NOTIFY;
+  send_event.crossing.window = window_at_pointer;
+  send_event.crossing.time = GDK_CURRENT_TIME;
+  send_event.crossing.send_event = TRUE;
+	  
+  gtk_widget_event (GTK_WIDGET (user_data), &send_event);
+
+  return FALSE; 
+}
+
+static gboolean
+gtk_menu_navigating_submenu (gint event_x, gint event_y)
+{
+  if (navigation_region)
+    {
+      if (gdk_region_point_in (navigation_region, event_x, event_y))
+	return TRUE;
+      else
+	{
+	  gtk_menu_stop_navigating_submenu ();
+	  return FALSE;
+	}
+    }
+  return FALSE;
+}
+
+static void
+gtk_menu_set_submenu_navigation_region (GtkMenuItem      *menu_item,
+					GdkEventCrossing *event)
+{
+  gint submenu_left = 0, submenu_right  = 0;
+  gint submenu_top  = 0, submenu_bottom = 0;
+  gint width        = 0, height         = 0;
+  GdkPoint point[3];
+  GtkWidget *event_widget;
+
+  g_return_if_fail (menu_item->submenu != NULL);
+  g_return_if_fail (event != NULL);
+  
+  event_widget = gtk_get_event_widget ((GdkEvent*) event);
+  
+  gdk_window_get_root_origin (menu_item->submenu->window,
+      &submenu_left, &submenu_top);
+  gdk_window_get_size (menu_item->submenu->window, &width, &height);
+  submenu_right = submenu_left + width;
+  submenu_bottom = submenu_top + height;
+  gdk_window_get_size (event_widget->window, &width, &height);
+  
+  if ((event->x >= 0) && (event->x <= width)
+      && (((event->y < 0) && (event->y_root >= submenu_top))
+	  || ((event->y >= 0) && (event->y_root <= submenu_bottom))))
+    {
+      /* Set navigation region */
+      /* We need to give a little padding, because sometimes the pt-in-region function 
+	 won't count one of the polygon vertices (which the mouse pointer will be on 
+	 immediately) as being IN the region. */ 
+      if (menu_item->submenu_direction == GTK_DIRECTION_RIGHT)
+	point[0].x = event->x_root - SUBMENU_NAV_REGION_PADDING; 
+      else                             
+	point[0].x = event->x_root + SUBMENU_NAV_REGION_PADDING;  
+      
+      if (event->y < 0)
+	point[0].y = event->y_root + SUBMENU_NAV_REGION_PADDING;
+      else
+	point[0].y = event->y_root - SUBMENU_NAV_REGION_PADDING; 
+      
+      if (event->y < 0) /* Exiting the top */
+	point[1].y = submenu_top;
+      else /* Exiting the bottom */
+	point[1].y = submenu_bottom;
+      
+      if (menu_item->submenu_direction == GTK_DIRECTION_RIGHT)
+	point[1].x = submenu_left; /* submenu is on the right */
+      else /* submenu is on the left */
+	point[1].x = submenu_right;
+      
+      point[2].x = point[1].x;
+      point[2].y = point[0].y;
+      
+      navigation_region = gdk_region_polygon (point, 3, GDK_WINDING_RULE);
+
+      gtk_timeout_add (SUBMENU_NAV_HYSTERESIS_TIMEOUT, 
+		       gtk_menu_stop_navigating_submenu_cb, menu_item); 
+    }
 }
 
 static void


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]