Submenu navigation patch for 1.2
- From: George <jirka 5z com>
- To: GTK Hackers <gtk-devel-list gnome org>
- Subject: Submenu navigation patch for 1.2
- Date: Tue, 5 Sep 2000 19:42:12 -0700
Here's a port of Nils Barth's submenu navigation patch for 1.2. For the two
extra GtkMenu fields it uses get/set_data. I think this is 1) simple enough
2) incredibly useful to add to the 1.2 branch. This has probably been the
number one usability complaint on the panel and I would bet for gtk as well.
It would be very nice to have this patch in whatever gtk comes with GNOME
1.4.
Here's the relevant changelog from head
Fri Sep 1 22:39:07 2000 Owen Taylor <otaylor redhat com>
* gtk/gtkmenu.[ch] TODO.xml: Apply patch from
Nils Barth and David Santiago to improve submenu
navigation. The patch does this by creating a triangular
region from the point where the pointer leaves the
menu to the submenu. While the pointer is in
that region and a timeout has not expired, events
that would cause the active submenu to change are
ignored.
I applied one minor fix that is explained in my next mail about HEAD as well.
George
--
George <jirka 5z com>
Man will occasionally stumble over the truth,
but most times he will pick himself up and carry on.
-- Winston Churchill
Index: gtkmenu.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenu.c,v
retrieving revision 1.41.2.3
diff -u -p -r1.41.2.3 gtkmenu.c
--- gtkmenu.c 1999/09/03 16:51:12 1.41.2.3
+++ gtkmenu.c 2000/09/06 02:35:29
@@ -38,6 +38,9 @@
#define MENU_ITEM_CLASS(w) GTK_MENU_ITEM_CLASS (GTK_OBJECT (w)->klass)
#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
@@ -47,23 +50,43 @@ struct _GtkMenuAttachData
};
-static void gtk_menu_class_init (GtkMenuClass *klass);
-static void gtk_menu_init (GtkMenu *menu);
-static void gtk_menu_destroy (GtkObject *object);
-static void gtk_menu_realize (GtkWidget *widget);
-static void gtk_menu_size_request (GtkWidget *widget,
- GtkRequisition *requisition);
-static void gtk_menu_size_allocate (GtkWidget *widget,
- GtkAllocation *allocation);
-static void gtk_menu_paint (GtkWidget *widget);
-static void gtk_menu_draw (GtkWidget *widget,
- GdkRectangle *area);
-static gint gtk_menu_expose (GtkWidget *widget,
- GdkEventExpose *event);
-static gint gtk_menu_key_press (GtkWidget *widget,
- GdkEventKey *event);
-static gint gtk_menu_motion_notify (GtkWidget *widget,
- GdkEventMotion *event);
+static void gtk_menu_class_init (GtkMenuClass *klass);
+static void gtk_menu_init (GtkMenu *menu);
+static void gtk_menu_destroy (GtkObject *object);
+static void gtk_menu_realize (GtkWidget *widget);
+static void gtk_menu_size_request (GtkWidget *widget,
+ GtkRequisition *requisition);
+static void gtk_menu_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation);
+static void gtk_menu_paint (GtkWidget *widget);
+static void gtk_menu_draw (GtkWidget *widget,
+ GdkRectangle *area);
+static gboolean gtk_menu_expose (GtkWidget *widget,
+ GdkEventExpose *event);
+static gboolean gtk_menu_key_press (GtkWidget *widget,
+ GdkEventKey *event);
+static gboolean gtk_menu_motion_notify (GtkWidget *widget,
+ GdkEventMotion *event);
+static gboolean gtk_menu_enter_notify (GtkWidget *widget,
+ GdkEventCrossing *event);
+static gboolean gtk_menu_leave_notify (GtkWidget *widget,
+ GdkEventCrossing *event);
+
+static void gtk_menu_stop_navigating_submenu (GtkMenu *menu);
+static gboolean gtk_menu_stop_navigating_submenu_cb (gpointer user_data);
+static gboolean gtk_menu_navigating_submenu (GtkMenu *menu,
+ gint event_x,
+ gint event_y);
+static void gtk_menu_set_submenu_navigation_region (GtkMenu *menu,
+ GtkMenuItem *menu_item,
+ GdkEventCrossing *event);
+static GdkRegion *gtk_menu_get_navigation_region (GtkMenu *menu);
+static void gtk_menu_set_navigation_region (GtkMenu *menu,
+ GdkRegion *region);
+static guint gtk_menu_get_navigation_timeout (GtkMenu *menu);
+static void gtk_menu_set_navigation_timeout (GtkMenu *menu,
+ guint timeout);
+
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,6 +98,8 @@ static void gtk_menu_reparent (Gtk
static GtkMenuShellClass *parent_class = NULL;
static const gchar *attach_data_key = "gtk-menu-attach-data";
static GQuark quark_uline_accel_group = 0;
+static GQuark quark_navigation_region = 0;
+static GQuark quark_navigation_timeout = 0;
GtkType
@@ -129,6 +154,8 @@ gtk_menu_class_init (GtkMenuClass *class
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;
@@ -156,7 +183,7 @@ gtk_menu_class_init (GtkMenuClass *class
GTK_MENU_DIR_CHILD);
}
-static gint
+static gboolean
gtk_menu_window_event (GtkWidget *window,
GdkEvent *event,
GtkWidget *menu)
@@ -230,6 +257,8 @@ gtk_menu_destroy (GtkObject *object)
if (data)
gtk_menu_detach (menu);
+ gtk_menu_stop_navigating_submenu (menu);
+
gtk_menu_set_accel_group (menu, NULL);
if (menu->old_active_menu_item)
@@ -525,6 +554,8 @@ gtk_menu_popdown (GtkMenu *menu)
menu_shell->active = FALSE;
menu_shell->ignore_enter = FALSE;
+ gtk_menu_stop_navigating_submenu (menu);
+
if (menu_shell->active_menu_item)
{
if (menu->old_active_menu_item)
@@ -976,7 +1007,7 @@ gtk_menu_draw (GtkWidget *widget,
}
}
-static gint
+static gboolean
gtk_menu_expose (GtkWidget *widget,
GdkEventExpose *event)
{
@@ -1014,7 +1045,7 @@ gtk_menu_expose (GtkWidget *widget,
return FALSE;
}
-static gint
+static gboolean
gtk_menu_key_press (GtkWidget *widget,
GdkEventKey *event)
{
@@ -1027,6 +1058,8 @@ gtk_menu_key_press (GtkWidget *widget,
menu_shell = GTK_MENU_SHELL (widget);
+ gtk_menu_stop_navigating_submenu (GTK_MENU (widget));
+
if (GTK_WIDGET_CLASS (parent_class)->key_press_event (widget, event))
return TRUE;
@@ -1100,19 +1133,51 @@ gtk_menu_key_press (GtkWidget *widget,
return TRUE;
}
-static gint
+static gboolean
gtk_menu_motion_notify (GtkWidget *widget,
GdkEventMotion *event)
{
- g_return_val_if_fail (widget != NULL, FALSE);
- g_return_val_if_fail (GTK_IS_MENU (widget), FALSE);
+ GtkWidget *menu_item;
+ GtkMenu *menu;
+ GdkRegion *navigation_region;
+ GtkMenuShell *menu_shell;
+
+ gboolean need_enter;
+
+ /* We received the event for one of two reasons:
+ *
+ * a) We are the active menu, and did gtk_grab_add()
+ * b) The widget is a child of ours, and the event was propagated
+ *
+ * Since for computation of navigation regions, we want the menu which
+ * is the parent of the menu item, for a), we need to find that menu,
+ * which may be different from 'widget'.
+ */
+
+ menu_item = gtk_get_event_widget ((GdkEvent*) event);
+ if (!menu_item || !GTK_IS_MENU_ITEM (menu_item) || !GTK_WIDGET_IS_SENSITIVE (menu_item) ||
+ !GTK_IS_MENU (menu_item->parent))
+ return FALSE;
+
+ menu_shell = GTK_MENU_SHELL (menu_item->parent);
+ menu = GTK_MENU (menu_shell);
+
+ navigation_region = gtk_menu_get_navigation_region (menu);
- if (GTK_MENU_SHELL (widget)->ignore_enter)
- GTK_MENU_SHELL (widget)->ignore_enter = FALSE;
- else
+ need_enter = (navigation_region != NULL || menu_shell->ignore_enter);
+
+ /* Check to see if we are within an active submenu's navigation region
+ */
+ if (gtk_menu_navigating_submenu (menu, event->x_root, event->y_root))
+ return TRUE;
+
+ if (need_enter)
{
+ /* The menu is now sensitive to enter events on its items, but
+ * was previously sensitive. So we fake an enter event.
+ */
gint width, height;
-
+
gdk_window_get_size (event->window, &width, &height);
if (event->x >= 0 && event->x < width &&
event->y >= 0 && event->y < height)
@@ -1123,15 +1188,302 @@ gtk_menu_motion_notify (GtkWidget *w
send_event.crossing.window = event->window;
send_event.crossing.time = event->time;
send_event.crossing.send_event = TRUE;
-
- gtk_widget_event (widget, &send_event);
+ send_event.crossing.x_root = event->x_root;
+ send_event.crossing.y_root = event->y_root;
+ send_event.crossing.x = event->x;
+ send_event.crossing.y = event->y;
+
+ /* We send the event to 'widget', the currently active menu,
+ * instead of 'menu', the menu that the pointer is in. This
+ * will ensure that the event will be ignored unless the
+ * menuitem is a child of the active menu or some parent
+ * menu of the active menu.
+ */
+ return gtk_widget_event (widget, &send_event);
}
+
+ menu_shell->ignore_enter = FALSE;
}
return FALSE;
}
+static gboolean
+gtk_menu_enter_notify (GtkWidget *widget,
+ GdkEventCrossing *event)
+{
+ GtkWidget *menu_item;
+
+ /* If this is a faked enter (see gtk_menu_motion_notify), 'widget'
+ * will not correspond to the event widget's parent. Check to see
+ * if we are in the parent's navigation region.
+ */
+ menu_item = gtk_get_event_widget ((GdkEvent*) event);
+ if (menu_item && GTK_IS_MENU_ITEM (menu_item) && GTK_IS_MENU (menu_item->parent) &&
+ gtk_menu_navigating_submenu (GTK_MENU (menu_item->parent), event->x_root, event->y_root))
+ return TRUE;
+
+ return GTK_WIDGET_CLASS (parent_class)->enter_notify_event (widget, event);
+}
+
+static gboolean
+gtk_menu_leave_notify (GtkWidget *widget,
+ GdkEventCrossing *event)
+{
+ GtkMenuShell *menu_shell;
+ GtkMenu *menu;
+ GtkMenuItem *menu_item;
+ GtkWidget *event_widget;
+
+ menu = GTK_MENU (widget);
+ menu_shell = GTK_MENU_SHELL (widget);
+
+ if (gtk_menu_navigating_submenu (menu, event->x_root, event->y_root))
+ return TRUE;
+
+ 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);
+
+ /* 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 (menu_shell->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, menu_item, event);
+ return TRUE;
+ }
+ }
+
+ return GTK_WIDGET_CLASS (parent_class)->leave_notify_event (widget, event);
+}
+
+static void
+gtk_menu_stop_navigating_submenu (GtkMenu *menu)
+{
+ GdkRegion *navigation_region;
+ guint navigation_timeout;
+
+ navigation_region = gtk_menu_get_navigation_region (menu);
+ navigation_timeout = gtk_menu_get_navigation_timeout (menu);
+
+ if (navigation_region)
+ {
+ gdk_region_destroy (navigation_region);
+ gtk_menu_set_navigation_region (menu, NULL);
+ }
+
+ if (navigation_timeout)
+ {
+ gtk_timeout_remove (navigation_timeout);
+ gtk_menu_set_navigation_timeout (menu, 0);
+ }
+}
+
+/* When the timeout is elapsed, the navigation region is destroyed
+ * and the menuitem under the pointer (if any) is selected.
+ */
+static gboolean
+gtk_menu_stop_navigating_submenu_cb (gpointer user_data)
+{
+ GdkEventCrossing send_event;
+
+ GtkMenu *menu = user_data;
+ GdkWindow *child_window;
+
+ gtk_menu_stop_navigating_submenu (menu);
+
+ if (GTK_WIDGET_REALIZED (menu))
+ {
+ child_window = gdk_window_get_pointer (GTK_WIDGET (menu)->window, NULL, NULL, NULL);
+
+ if (child_window)
+ {
+ send_event.window = child_window;
+ send_event.type = GDK_ENTER_NOTIFY;
+ send_event.time = GDK_CURRENT_TIME; /* Bogus */
+ send_event.send_event = TRUE;
+
+ GTK_WIDGET_CLASS (parent_class)->enter_notify_event (GTK_WIDGET (menu), &send_event);
+ }
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gtk_menu_navigating_submenu (GtkMenu *menu,
+ gint event_x,
+ gint event_y)
+{
+ GdkRegion *navigation_region;
+
+ navigation_region = gtk_menu_get_navigation_region (menu);
+
+ if (navigation_region)
+ {
+ if (gdk_region_point_in (navigation_region, event_x, event_y))
+ return TRUE;
+ else
+ {
+ gtk_menu_stop_navigating_submenu (menu);
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+gtk_menu_set_submenu_navigation_region (GtkMenu *menu,
+ GtkMenuItem *menu_item,
+ GdkEventCrossing *event)
+{
+ gint submenu_left = 0;
+ gint submenu_right = 0;
+ gint submenu_top = 0;
+ gint submenu_bottom = 0;
+ gint width = 0;
+ gint height = 0;
+ GdkPoint point[3];
+ GtkWidget *event_widget;
+ GdkRegion *navigation_region;
+ guint navigation_timeout;
+
+ 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_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)
+ {
+ /* Set navigation region */
+ /* We fudge/give a little padding in case the user
+ * ``misses the vertex'' of the triangle/is off by a pixel or two.
+ */
+ 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;
+
+ /* Exiting the top or bottom? */
+ if (event->y < 0)
+ { /* top */
+ point[0].y = event->y_root + SUBMENU_NAV_REGION_PADDING;
+ point[1].y = submenu_top;
+
+ if (point[0].y <= point[1].y)
+ return;
+ }
+ else
+ { /* bottom */
+ point[0].y = event->y_root - SUBMENU_NAV_REGION_PADDING;
+ point[1].y = submenu_bottom;
+
+ if (point[0].y >= point[1].y)
+ return;
+ }
+
+ /* Submenu is to the left or right? */
+ if (menu_item->submenu_direction == GTK_DIRECTION_RIGHT)
+ point[1].x = submenu_left; /* right */
+ else
+ point[1].x = submenu_right; /* left */
+
+ point[2].x = point[1].x;
+ point[2].y = point[0].y;
+
+ gtk_menu_stop_navigating_submenu (menu);
+
+ navigation_region = gdk_region_polygon (point, 3, GDK_WINDING_RULE);
+
+ gtk_menu_set_navigation_region (menu, navigation_region);
+
+ navigation_timeout = gtk_timeout_add (SUBMENU_NAV_HYSTERESIS_TIMEOUT,
+ gtk_menu_stop_navigating_submenu_cb, menu);
+
+ gtk_menu_set_navigation_timeout (menu, navigation_timeout);
+ }
+}
+
+static GdkRegion *
+gtk_menu_get_navigation_region (GtkMenu *menu)
+{
+ GdkRegion *region;
+
+ g_return_val_if_fail (GTK_IS_MENU (menu), NULL);
+
+ if (!quark_navigation_region)
+ quark_navigation_region = g_quark_from_static_string ("GtkMenu-navigation-region");
+
+ region = gtk_object_get_data_by_id (GTK_OBJECT (menu),
+ quark_navigation_region);
+
+ return region;
+}
+
static void
+gtk_menu_set_navigation_region (GtkMenu *menu,
+ GdkRegion *region)
+{
+ g_return_if_fail (GTK_IS_MENU (menu));
+
+ if (!quark_navigation_region)
+ quark_navigation_region = g_quark_from_static_string ("GtkMenu-navigation-region");
+
+ gtk_object_set_data_by_id (GTK_OBJECT (menu),
+ quark_navigation_region,
+ region);
+}
+
+static guint
+gtk_menu_get_navigation_timeout (GtkMenu *menu)
+{
+ gpointer data;
+
+ g_return_val_if_fail (GTK_IS_MENU (menu), 0);
+
+ if (!quark_navigation_timeout)
+ quark_navigation_timeout = g_quark_from_static_string ("GtkMenu-navigation-timeout");
+
+ data = gtk_object_get_data_by_id (GTK_OBJECT (menu),
+ quark_navigation_timeout);
+
+ return GPOINTER_TO_UINT (data);
+}
+
+static void
+gtk_menu_set_navigation_timeout (GtkMenu *menu,
+ guint timeout)
+{
+ gpointer data;
+
+ g_return_if_fail (GTK_IS_MENU (menu));
+
+ if (!quark_navigation_timeout)
+ quark_navigation_timeout = g_quark_from_static_string ("GtkMenu-navigation-timeout");
+
+ data = GUINT_TO_POINTER (timeout);
+
+ gtk_object_set_data_by_id (GTK_OBJECT (menu),
+ quark_navigation_timeout,
+ data);
+}
+
+static void
gtk_menu_deactivate (GtkMenuShell *menu_shell)
{
GtkWidget *parent;
@@ -1244,4 +1596,3 @@ gtk_menu_hide_all (GtkWidget *widget)
/* Hide children, but not self. */
gtk_container_foreach (GTK_CONTAINER (widget), (GtkCallback) gtk_widget_hide_all, NULL);
}
-
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]