[Patch] Scrolling menus



Ok, here is my attempt at adding scrolling menus to Gtk+.
It is lightly based on a patch by samuel animeta com posted earlier on
this list and on the comments owed had on it.

There is currently two bugs:
* I don't seem to get any button press event in the tearoff window, so you
can't scroll in the tearoff window.
* I don't get an enter_notify event when entering the menu at a scroll
arrow, unless you go via a menu item.

I will continue to try and fix these problems, but I post this so I can
get some general comments on the rest of the patch.

/ Alex

Index: gtkitemfactory.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkitemfactory.c,v
retrieving revision 1.31
diff -u -p -r1.31 gtkitemfactory.c
--- gtkitemfactory.c	2000/07/26 11:32:44	1.31
+++ gtkitemfactory.c	2000/10/10 15:36:26
@@ -1386,6 +1386,7 @@ static void
 gtk_item_factory_menu_pos (GtkMenu  *menu,
 			   gint     *x,
 			   gint     *y,
+			   gint     *scroll_offset,
 			   gpointer  func_data)
 {
   MenuPos *mpos = func_data;
Index: gtkmenu.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenu.c,v
retrieving revision 1.49
diff -u -p -r1.49 gtkmenu.c
--- gtkmenu.c	2000/09/08 01:53:46	1.49
+++ gtkmenu.c	2000/10/10 15:36:27
@@ -41,6 +41,9 @@
 #define SUBMENU_NAV_REGION_PADDING 2
 #define SUBMENU_NAV_HYSTERESIS_TIMEOUT 333
 
+#define MENU_SCROLL_ARROW_HEIGHT 16
+#define MENU_SCROLL_TIMEOUT 200
+
 typedef struct _GtkMenuAttachData	GtkMenuAttachData;
 
 struct _GtkMenuAttachData
@@ -50,27 +53,38 @@ 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 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_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_unrealize      (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_button_press   (GtkWidget        *widget,
+					 GdkEventButton   *event);
+static gboolean gtk_menu_button_release (GtkWidget        *widget,
+					 GdkEventButton   *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_scroll_to      (GtkMenu          *menu,
+					 guint             position);
+static gboolean gtk_menu_scroll_timeout (gpointer          data);
+static gint     gtk_menu_on_arrow       (GtkMenu          *menu);
+static void gtk_menu_select_item        (GtkMenuShell    *menu_shell,
+					 GtkWidget       *menu_item);
 
 static void     gtk_menu_stop_navigating_submenu       (GtkMenu          *menu);
 static gboolean gtk_menu_stop_navigating_submenu_cb    (gpointer          user_data);
@@ -137,11 +151,14 @@ gtk_menu_class_init (GtkMenuClass *class
   object_class->destroy = gtk_menu_destroy;
   
   widget_class->realize = gtk_menu_realize;
+  widget_class->unrealize = gtk_menu_unrealize;
   widget_class->draw = gtk_menu_draw;
   widget_class->size_request = gtk_menu_size_request;
   widget_class->size_allocate = gtk_menu_size_allocate;
   widget_class->expose_event = gtk_menu_expose;
   widget_class->key_press_event = gtk_menu_key_press;
+  widget_class->button_press_event = gtk_menu_button_press;
+  widget_class->button_release_event = gtk_menu_button_release;
   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;
@@ -150,6 +167,7 @@ gtk_menu_class_init (GtkMenuClass *class
   
   menu_shell_class->submenu_placement = GTK_LEFT_RIGHT;
   menu_shell_class->deactivate = gtk_menu_deactivate;
+  menu_shell_class->select_item = gtk_menu_select_item;
 
   binding_set = gtk_binding_set_by_class (class);
   gtk_binding_entry_add_signal (binding_set,
@@ -188,6 +206,8 @@ gtk_menu_window_event (GtkWidget *window
     {
     case GDK_KEY_PRESS:
     case GDK_KEY_RELEASE:
+    case GDK_BUTTON_PRESS:
+    case GDK_BUTTON_RELEASE:
       gtk_widget_event (menu, event);
       handled = TRUE;
       break;
@@ -225,9 +245,21 @@ gtk_menu_init (GtkMenu *menu)
   GTK_WIDGET_SET_FLAGS (menu, GTK_FLOATING);
   menu->needs_destruction_ref_count = TRUE;
 
+  menu->view_window = NULL;
+  menu->item_window = NULL;
+
+  menu->first_item = 0;
+  menu->scrolling_direction  = 0;
+  menu->timeout_id = 0;
+  
   menu->tearoff_window = NULL;
   menu->torn_off = FALSE;
 
+  menu->upper_arrow_visible = FALSE;
+  menu->lower_arrow_visible = FALSE;
+  menu->upper_arrow_prelight = FALSE;
+  menu->lower_arrow_prelight = FALSE;
+  
   MENU_NEEDS_RESIZE (menu) = TRUE;
 }
 
@@ -241,6 +273,9 @@ gtk_menu_destroy (GtkObject *object)
 
   menu = GTK_MENU (object);
   
+  if (menu->timeout_id)
+    g_source_remove (menu->timeout_id);
+  
   data = gtk_object_get_data (object, attach_data_key);
   if (data)
     gtk_menu_detach (menu);
@@ -385,6 +420,7 @@ static void
 gtk_menu_tearoff_bg_copy (GtkMenu *menu)
 {
   GtkWidget *widget;
+  gint width, height;
 
   widget = GTK_WIDGET (menu);
 
@@ -398,9 +434,11 @@ gtk_menu_tearoff_bg_copy (GtkMenu *menu)
       gc = gdk_gc_new_with_values (widget->window,
 				   &gc_values, GDK_GC_SUBWINDOW);
       
+      gdk_window_get_size (widget->window, &width, &height);
+      
       pixmap = gdk_pixmap_new (widget->window,
-			       widget->requisition.width,
-			       widget->requisition.height,
+			       width,
+			       height,
 			       -1);
 
       gdk_draw_pixmap (pixmap, gc,
@@ -409,9 +447,9 @@ gtk_menu_tearoff_bg_copy (GtkMenu *menu)
       gdk_gc_unref (gc);
       
       gtk_widget_set_usize (menu->tearoff_window,
-			    widget->requisition.width,
-			    widget->requisition.height);
-      
+			    width,
+			    height);
+
       gdk_window_set_back_pixmap (menu->tearoff_window->window, pixmap, FALSE);
       gdk_pixmap_unref (pixmap);
     }
@@ -527,6 +565,8 @@ gtk_menu_popup (GtkMenu		    *menu,
       gdk_cursor_destroy (cursor);
     }
   
+  gtk_menu_scroll_to (menu, menu->first_item);
+
   gtk_grab_add (GTK_WIDGET (menu));
 }
 
@@ -534,7 +574,7 @@ void
 gtk_menu_popdown (GtkMenu *menu)
 {
   GtkMenuShell *menu_shell;
-  
+
   g_return_if_fail (menu != NULL);
   g_return_if_fail (GTK_IS_MENU (menu));
   
@@ -543,6 +583,12 @@ gtk_menu_popdown (GtkMenu *menu)
   menu_shell->parent_menu_shell = NULL;
   menu_shell->active = FALSE;
   menu_shell->ignore_enter = FALSE;
+
+  if (menu->timeout_id)
+    {
+      g_source_remove (menu->timeout_id);
+      menu->timeout_id = 0;
+    }
   
   gtk_menu_stop_navigating_submenu (menu);
   
@@ -769,6 +815,9 @@ gtk_menu_set_tearoff_state (GtkMenu  *me
 	  
 	  gtk_widget_show (GTK_WIDGET (menu));
 	  gtk_widget_show (menu->tearoff_window);
+
+	  gtk_menu_scroll_to (menu, menu->first_item);
+
 	}
       else
 	{
@@ -812,9 +861,16 @@ gtk_menu_realize (GtkWidget *widget)
 {
   GdkWindowAttr attributes;
   gint attributes_mask;
-  
+  gint border_width;
+  gint event_mask;
+  GtkMenu *menu;
+  GtkWidget *child;
+  GList *children;
+
   g_return_if_fail (widget != NULL);
   g_return_if_fail (GTK_IS_MENU (widget));
+
+  menu = GTK_MENU (widget);
   
   GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);
   
@@ -826,19 +882,102 @@ gtk_menu_realize (GtkWidget *widget)
   attributes.wclass = GDK_INPUT_OUTPUT;
   attributes.visual = gtk_widget_get_visual (widget);
   attributes.colormap = gtk_widget_get_colormap (widget);
-  attributes.event_mask = gtk_widget_get_events (widget);
-  attributes.event_mask |= (GDK_EXPOSURE_MASK | GDK_KEY_PRESS_MASK);
+  
+  event_mask = gtk_widget_get_events (widget);
+  event_mask |= (GDK_EXPOSURE_MASK | GDK_KEY_PRESS_MASK);
+  attributes.event_mask = event_mask;
   
   attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;
   widget->window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask);
   gdk_window_set_user_data (widget->window, widget);
   
+  border_width = GTK_CONTAINER (widget)->border_width;
+  
+  attributes.x += border_width + widget->style->xthickness;
+  attributes.y += border_width + widget->style->ythickness;
+  attributes.width = MAX (1, (gint)widget->allocation.width - border_width * 2 - widget->style->xthickness * 2);
+  attributes.height = MAX (1, (gint)widget->allocation.height - border_width * 2 - widget->style->ythickness * 2);
+  attributes.event_mask = 0;
+
+  if (menu->upper_arrow_visible)
+    {
+      attributes.y += MENU_SCROLL_ARROW_HEIGHT;
+      attributes.height -= MENU_SCROLL_ARROW_HEIGHT;
+    }
+  if (menu->lower_arrow_visible)
+    attributes.height -= MENU_SCROLL_ARROW_HEIGHT;
+
+  menu->view_window = gdk_window_new (widget->window, &attributes, attributes_mask);
+  gdk_window_set_user_data (menu->view_window, menu);
+
+  attributes.x = 0;
+  attributes.y = 0;
+  attributes.event_mask = event_mask;
+
+  /* Calculate height of the item_window. Start with the height of
+   * view_window so that we get extra space that can show up at the
+   * end when the menu is scrolled so that the last item is visible.
+   */
+  children = GTK_MENU_SHELL (menu)->children;
+  while (children)
+    {
+      child = children->data;
+      children = children->next;
+	  
+      if (GTK_WIDGET_VISIBLE (child))
+	{
+	  GtkRequisition child_requisition;
+	  gtk_widget_get_child_requisition (child, &child_requisition);
+	  
+	  attributes.height += child_requisition.height;
+	}
+    }
+  
+  menu->item_window = gdk_window_new (menu->view_window, &attributes, attributes_mask);
+  gdk_window_set_user_data (menu->item_window, menu);
+
+  children = GTK_MENU_SHELL (menu)->children;
+  while (children)
+    {
+      child = children->data;
+      children = children->next;
+	  
+      gtk_widget_set_parent_window (child, menu->item_window);
+    }
+  
   widget->style = gtk_style_attach (widget->style, widget->window);
+  gtk_style_set_background (widget->style, menu->item_window, GTK_STATE_NORMAL);
   gtk_style_set_background (widget->style, widget->window, GTK_STATE_NORMAL);
+
   gtk_menu_paint(widget);
+  
+  gdk_window_show (menu->item_window);
+  gdk_window_show (menu->view_window);
 }
 
 static void
+gtk_menu_unrealize (GtkWidget *widget)
+{
+  GtkMenu *menu;
+
+  g_return_if_fail (widget != NULL);
+  g_return_if_fail (GTK_IS_MENU (widget));
+
+  menu = GTK_MENU (widget);
+
+  gdk_window_set_user_data (menu->view_window, NULL);
+  gdk_window_destroy (menu->view_window);
+  menu->view_window = NULL;
+
+  gdk_window_set_user_data (menu->item_window, NULL);
+  gdk_window_destroy (menu->item_window);
+  menu->item_window = NULL;
+
+  if (GTK_WIDGET_CLASS (parent_class)->unrealize)
+    (* GTK_WIDGET_CLASS (parent_class)->unrealize) (widget);
+}
+
+static void
 gtk_menu_size_request (GtkWidget      *widget,
 		       GtkRequisition *requisition)
 {
@@ -907,28 +1046,51 @@ gtk_menu_size_allocate (GtkWidget     *w
   GtkWidget *child;
   GtkAllocation child_allocation;
   GList *children;
-  
+  gint x, y;
+  gint width, height;
+
   g_return_if_fail (widget != NULL);
   g_return_if_fail (GTK_IS_MENU (widget));
   g_return_if_fail (allocation != NULL);
   
   menu = GTK_MENU (widget);
   menu_shell = GTK_MENU_SHELL (widget);
-  
+
   widget->allocation = *allocation;
-  if (GTK_WIDGET_REALIZED (widget))
-    gdk_window_move_resize (widget->window,
-			    allocation->x, allocation->y,
-			    allocation->width, allocation->height);
 
+  x = (GTK_CONTAINER (menu)->border_width + widget->style->xthickness);
+  y = (GTK_CONTAINER (menu)->border_width + widget->style->ythickness);
+  
+  width = MAX (1, (gint)allocation->width - x * 2);
+  height = MAX (1, (gint)allocation->height - y * 2);
+  
+  if (menu->upper_arrow_visible)
+    {
+      y += MENU_SCROLL_ARROW_HEIGHT;
+      height -= MENU_SCROLL_ARROW_HEIGHT;
+    }
+  
+  if (menu->lower_arrow_visible)
+    height -= MENU_SCROLL_ARROW_HEIGHT;
+  
+  if (GTK_WIDGET_REALIZED (widget))
+    {
+      gdk_window_move_resize (widget->window,
+			      allocation->x, allocation->y,
+			      allocation->width, allocation->height);
+
+      gdk_window_move_resize (menu->view_window,
+			      x,
+			      y,
+			      width,
+			      height);
+    }
 
   if (menu_shell->children)
     {
-      child_allocation.x = (GTK_CONTAINER (menu)->border_width +
-			    widget->style->xthickness);
-      child_allocation.y = (GTK_CONTAINER (menu)->border_width +
-			    widget->style->ythickness);
-      child_allocation.width = MAX (1, (gint)allocation->width - child_allocation.x * 2);
+      child_allocation.x = 0;
+      child_allocation.y = 0;
+      child_allocation.width = width;
       
       children = menu_shell->children;
       while (children)
@@ -949,14 +1111,29 @@ gtk_menu_size_allocate (GtkWidget     *w
 	      child_allocation.y += child_allocation.height;
 	    }
 	}
+      
+      /* Resize the item window */
+      if (GTK_WIDGET_REALIZED (widget))
+	{
+	  gdk_window_resize (menu->item_window,
+			     child_allocation.width,
+			     child_allocation.y + allocation->height);
+	}
+
     }
 }
 
 static void
 gtk_menu_paint (GtkWidget *widget)
 {
+  guint border;
+  guint width, height;
+  GtkMenu *menu;
+  
   g_return_if_fail (widget != NULL);
   g_return_if_fail (GTK_IS_MENU (widget));
+
+  menu = GTK_MENU(widget);
   
   if (GTK_WIDGET_DRAWABLE (widget))
     {
@@ -966,7 +1143,43 @@ gtk_menu_paint (GtkWidget *widget)
 		     GTK_SHADOW_OUT,
 		     NULL, widget, "menu",
 		     0, 0, -1, -1);
+
+      border = GTK_CONTAINER (widget)->border_width + widget->style->ythickness + 1;
+      gdk_window_get_size (widget->window, &width, &height);
+
+      if (menu->upper_arrow_visible)
+	{
+	  gtk_paint_arrow (widget->style,
+			   widget->window,
+			   menu->upper_arrow_prelight ?
+			   GTK_STATE_PRELIGHT : GTK_STATE_NORMAL,
+			   GTK_SHADOW_OUT,
+			   NULL, widget, "menu",
+			   GTK_ARROW_UP,
+			   TRUE,
+			   width / 2 - MENU_SCROLL_ARROW_HEIGHT / 2 + 1,
+			   border - 1,
+			   MENU_SCROLL_ARROW_HEIGHT - 2,
+			   MENU_SCROLL_ARROW_HEIGHT - 2);
+	}
+  
+      if (menu->lower_arrow_visible)
+	{
+	  gtk_paint_arrow (widget->style,
+			   widget->window,
+			   menu->lower_arrow_prelight ?
+			   GTK_STATE_PRELIGHT : GTK_STATE_NORMAL,
+			   GTK_SHADOW_OUT,
+			   NULL, widget, "menu",
+			   GTK_ARROW_DOWN,
+			   TRUE,
+			   width / 2 - MENU_SCROLL_ARROW_HEIGHT / 2 + 1,
+			   height - border - MENU_SCROLL_ARROW_HEIGHT + 1,
+			   MENU_SCROLL_ARROW_HEIGHT - 2,
+			   MENU_SCROLL_ARROW_HEIGHT - 2);
+	}
     }
+
 }
 
 static void
@@ -1038,13 +1251,106 @@ gtk_menu_expose (GtkWidget	*widget,
   return FALSE;
 }
 
+static gint
+gtk_menu_on_arrow (GtkMenu          *menu)
+{
+  gint width, height;
+  gint x, y;
+  guint border;
+  GtkWidget *widget;
+
+  widget = GTK_WIDGET (menu);
+  
+  gdk_window_get_pointer (widget->window, &x, &y, NULL);
+  gdk_window_get_size (GTK_WIDGET (menu)->window, &width, &height);
+
+  border = GTK_CONTAINER (menu)->border_width + GTK_WIDGET (menu)->style->ythickness;
+  if (menu->upper_arrow_visible)
+    {
+      if ((x >= 0) && (x < width) &&
+	  (y >= border) && (y < border + MENU_SCROLL_ARROW_HEIGHT))
+	{
+	  return -1;
+	}
+    }
+  
+  if (menu->lower_arrow_visible)
+    {
+      if ((x >= 0) && (x < width) &&
+	  (y >= height - border - MENU_SCROLL_ARROW_HEIGHT) && (y < height - border))
+	{
+	  return 1;
+	}
+    }
+  
+  return FALSE;
+}
+
+static gboolean
+gtk_menu_button_press (GtkWidget      *widget,
+		       GdkEventButton *event)
+{
+  GtkMenu *menu;
+  
+  g_return_val_if_fail (widget != NULL, FALSE);
+  g_return_val_if_fail (GTK_IS_MENU (widget), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+      
+  menu = GTK_MENU (widget);
+
+  if ((GTK_MENU_SHELL(menu)->active) &&
+      (!GTK_MENU_SHELL(menu)->button))
+    {
+      
+      if (gtk_menu_on_arrow (menu))
+	return TRUE;
+	  
+    }
+  
+  if (GTK_WIDGET_CLASS (parent_class)->button_press_event)
+    return (* GTK_WIDGET_CLASS (parent_class)->button_press_event) (widget, event);
+  
+  return FALSE;
+}
+
+static gboolean
+gtk_menu_button_release (GtkWidget      *widget,
+			 GdkEventButton *event)
+{
+  GtkMenu *menu;
+  gint dir;
+  
+  g_return_val_if_fail (widget != NULL, FALSE);
+  g_return_val_if_fail (GTK_IS_MENU (widget), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+      
+  menu = GTK_MENU (widget);
+
+  if ((GTK_MENU_SHELL(menu)->active) &&
+      (!GTK_MENU_SHELL(menu)->button))
+    {
+      dir = gtk_menu_on_arrow (menu);
+      if (dir)
+	{
+	  gtk_menu_scroll_to (menu, menu->first_item + dir);
+	  return TRUE;
+	}
+    }
+  
+  if (GTK_WIDGET_CLASS (parent_class)->button_release_event)
+    return (* GTK_WIDGET_CLASS (parent_class)->button_release_event) (widget, event);
+  
+  return FALSE;
+}
+
+
 static gboolean
 gtk_menu_key_press (GtkWidget	*widget,
 		    GdkEventKey *event)
 {
   GtkMenuShell *menu_shell;
   gboolean delete = FALSE;
-  
+
   g_return_val_if_fail (widget != NULL, FALSE);
   g_return_val_if_fail (GTK_IS_MENU (widget), FALSE);
   g_return_val_if_fail (event != NULL, FALSE);
@@ -1136,6 +1442,7 @@ gtk_menu_motion_notify  (GtkWidget	   *w
 
   gboolean need_enter;
 
+  
   /* We received the event for one of two reasons:
    *
    * a) We are the active menu, and did gtk_grab_add()
@@ -1145,7 +1452,6 @@ gtk_menu_motion_notify  (GtkWidget	   *w
    * 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))
@@ -1199,11 +1505,111 @@ gtk_menu_motion_notify  (GtkWidget	   *w
 }
 
 static gboolean
+gtk_menu_scroll_timeout (gpointer  data)
+{
+  GtkMenu *menu;
+  
+  menu = GTK_MENU (data);
+
+  gtk_menu_scroll_to (menu, menu->first_item + menu->scrolling_direction);
+
+  return TRUE;
+}
+
+static void
+gtk_menu_handle_scrolling(GtkMenu *menu)
+{
+  GtkMenuShell *menu_shell;
+  gint width, height;
+  gint x, y;
+  guint border;
+  GdkRectangle rect;
+  gboolean in_arrow;
+  
+  menu_shell = GTK_MENU_SHELL (menu);
+
+  gdk_window_get_pointer (GTK_WIDGET (menu)->window, &x, &y, NULL);
+  gdk_window_get_size (GTK_WIDGET (menu)->window, &width, &height);
+
+  border = GTK_CONTAINER (menu)->border_width + GTK_WIDGET (menu)->style->ythickness;
+
+  if (menu->upper_arrow_visible)
+    {
+      rect.x = 0;
+      rect.y = border;
+      rect.width = width;
+      rect.height = MENU_SCROLL_ARROW_HEIGHT;
+      
+      in_arrow = FALSE;
+      if ((x >= rect.x) && (x < rect.x + rect.width) &&
+	  (y >= rect.y) && (y < rect.y + rect.height))
+	in_arrow = TRUE;
+	
+      if (in_arrow != menu->upper_arrow_prelight)
+	{
+	  menu->upper_arrow_prelight = in_arrow;
+	  gdk_window_invalidate_rect (GTK_WIDGET (menu)->window, &rect, FALSE);
+
+	  if (menu->timeout_id)
+	    {
+	      g_source_remove (menu->timeout_id);
+	      menu->timeout_id = 0;
+	      menu->scrolling_direction = 0;
+	    }
+
+	  if (in_arrow && menu_shell->button)
+	    {
+	      /* Deselect the active item so that any submenus are poped down */
+	      gtk_menu_shell_deselect (menu_shell);
+	      
+	      menu->scrolling_direction = -1;
+	      menu->timeout_id = g_timeout_add (MENU_SCROLL_TIMEOUT, gtk_menu_scroll_timeout, menu);
+	    }
+	}
+    }
+  
+  if (menu->lower_arrow_visible)
+    {
+      rect.x = 0;
+      rect.y = height - border - MENU_SCROLL_ARROW_HEIGHT;
+      rect.width = width;
+      rect.height = MENU_SCROLL_ARROW_HEIGHT;
+
+      in_arrow = FALSE;
+      if ((x >= rect.x) && (x < rect.x + rect.width) &&
+	  (y >= rect.y) && (y < rect.y + rect.height))
+	in_arrow = TRUE;
+
+      if (in_arrow != menu->lower_arrow_prelight)
+	{
+	  menu->lower_arrow_prelight = in_arrow;
+	  gdk_window_invalidate_rect (GTK_WIDGET (menu)->window, &rect, FALSE);
+
+	  if (menu->timeout_id)
+	    {
+	      g_source_remove (menu->timeout_id);
+	      menu->timeout_id = 0;
+	      menu->scrolling_direction = 0;
+	    }
+
+	  if (in_arrow && menu_shell->button)
+	    {
+	      /* Deselect the active item so that any submenus are poped down */
+	      gtk_menu_shell_deselect (menu_shell);
+	      
+	      menu->scrolling_direction = 1;
+	      menu->timeout_id = g_timeout_add (MENU_SCROLL_TIMEOUT, gtk_menu_scroll_timeout, menu);
+	    }
+	}
+    }
+}
+
+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.
@@ -1213,6 +1619,9 @@ gtk_menu_enter_notify (GtkWidget        
       gtk_menu_navigating_submenu (GTK_MENU (menu_item->parent), event->x_root, event->y_root))
     return TRUE; 
 
+  if (widget && GTK_IS_MENU(widget))
+    gtk_menu_handle_scrolling (GTK_MENU(widget));
+      
   return GTK_WIDGET_CLASS (parent_class)->enter_notify_event (widget, event); 
 }
 
@@ -1223,13 +1632,15 @@ gtk_menu_leave_notify (GtkWidget        
   GtkMenuShell *menu_shell;
   GtkMenu *menu;
   GtkMenuItem *menu_item;
-  GtkWidget *event_widget; 
+  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; 
+
+  gtk_menu_handle_scrolling (menu);
   
   event_widget = gtk_get_event_widget ((GdkEvent*) event);
   
@@ -1416,12 +1827,18 @@ gtk_menu_position (GtkMenu *menu)
   GtkWidget *widget;
   GtkRequisition requisition;
   gint x, y;
+  gint screen_width;
+  gint screen_height;
+  gint scroll_pixels;
  
   g_return_if_fail (menu != NULL);
   g_return_if_fail (GTK_IS_MENU (menu));
 
   widget = GTK_WIDGET (menu);
 
+  screen_width = gdk_screen_width ();
+  screen_height = gdk_screen_height ();
+
   gdk_window_get_pointer (NULL, &x, &y, NULL);
 
   /* We need the requisition to figure out the right place to
@@ -1430,21 +1847,56 @@ gtk_menu_position (GtkMenu *menu)
    * the requisition won't have been recomputed yet.
    */
   gtk_widget_size_request (widget, &requisition);
-      
+
+  scroll_pixels = -1;
   if (menu->position_func)
-    (* menu->position_func) (menu, &x, &y, menu->position_func_data);
+    (* menu->position_func) (menu, &x, &y, &scroll_pixels, menu->position_func_data);
   else
     {
-      gint screen_width;
-      gint screen_height;
-      
-      screen_width = gdk_screen_width ();
-      screen_height = gdk_screen_height ();
-	  
       x = CLAMP (x - 2, 0, MAX (0, screen_width - requisition.width));
       y = CLAMP (y - 2, 0, MAX (0, screen_height - requisition.height));
     }
 
+  if (y<0)
+    {
+      requisition.height += y;
+      y = 0;
+    }
+
+  if (y + requisition.height > screen_height)
+    {
+      requisition.height -=  y + requisition.height - screen_height;
+    }
+
+  if (scroll_pixels >= 0)
+    {
+      GtkWidget *child;
+      GtkRequisition requisition;
+      GList *children;
+      gint ypos;
+      guint pos;
+
+      pos = 0;
+      ypos = 0;
+      children = GTK_MENU_SHELL (menu)->children;
+      while (children && (ypos < scroll_pixels))
+	{
+	  child = children->data;
+	  
+	  if (GTK_WIDGET_VISIBLE (child))
+	    {
+	      gtk_widget_get_child_requisition (child, &requisition);
+	      ypos += requisition.height;
+	    }
+	  
+	  children = children->next;
+	  pos++;
+	}
+      menu->first_item = pos;
+    }
+  
+  
+  
   /* FIXME: The MAX() here is because gtk_widget_set_uposition
    * is broken. Once we provide an alternate interface that
    * allows negative values, then we can remove them.
@@ -1452,8 +1904,219 @@ gtk_menu_position (GtkMenu *menu)
   gtk_widget_set_uposition (GTK_MENU_SHELL (menu)->active ?
 			        menu->toplevel : menu->tearoff_window, 
 			    MAX (x, 0), MAX (y, 0));
+  gtk_widget_set_usize (GTK_MENU_SHELL (menu)->active ?
+			    menu->toplevel : menu->tearoff_window,
+			-1, requisition.height);
 }
 
+static void
+gtk_menu_scroll_to (GtkMenu *menu,
+		    guint    position)
+{
+  GtkWidget *widget;
+  GtkWidget *child;
+  GList *children;
+  GtkRequisition child_requisition;
+  guint offset;
+  guint i;
+  gint x, y;
+  guint view_width, view_height;
+  guint item_height;
+  gint border_width;
+  gboolean last_visible;
+  gint n_children;
+
+  if (position < 0)
+    position = 0;
+
+  n_children = g_list_length (GTK_MENU_SHELL (menu)->children);
+  if (position >= n_children)
+    position = n_children - 1;
+  
+  offset = 0;
+  item_height = 0;
+  i = 0;
+  children = GTK_MENU_SHELL (menu)->children;
+  while (children)
+    {
+      child = children->data;
+      children = children->next;
+      
+      if (GTK_WIDGET_VISIBLE (child))
+	{
+	  gtk_widget_size_request (child, &child_requisition);
+	  item_height += child_requisition.height;
+	  if (i < position)
+	    offset += child_requisition.height;
+	}
+      i++;
+    }
+
+  widget = GTK_WIDGET (menu);
+  border_width = GTK_CONTAINER (menu)->border_width;
+
+  /* Scroll the menu: */
+  gdk_window_move (menu->item_window, 0, -offset);
+
+  /* Move/resize the viewport according to arrows: */
+  gdk_window_get_size (widget->window, &view_width, &view_height);
+
+  view_height -= border_width * 2 + widget->style->ythickness * 2;
+
+  last_visible = menu->upper_arrow_visible;
+  menu->upper_arrow_visible = (position > 0);
+
+  if (menu->upper_arrow_visible)
+    view_height -= MENU_SCROLL_ARROW_HEIGHT;
+
+  if ( (last_visible != menu->upper_arrow_visible) &&
+       !menu->upper_arrow_visible)
+    {
+      menu->upper_arrow_prelight = FALSE;
+      
+      /* If we hid the upper arrow, possibly remove timeout */
+      if ((menu->scrolling_direction < 0) && menu->timeout_id)
+	{
+	  g_source_remove (menu->timeout_id);
+	  menu->timeout_id = 0;
+	  menu->scrolling_direction = 0;
+	}
+    }
+
+  last_visible = menu->lower_arrow_visible;
+  menu->lower_arrow_visible = (item_height - offset > view_height);
+
+  if (menu->lower_arrow_visible)
+    view_height -= MENU_SCROLL_ARROW_HEIGHT;
+
+  if ( (last_visible != menu->lower_arrow_visible) &&
+       !menu->lower_arrow_visible)
+    {
+      menu->lower_arrow_prelight = FALSE;
+      
+      /* If we hid the lower arrow, possibly remove timeout */
+      if ((menu->scrolling_direction > 0) && menu->timeout_id)
+	{
+	  g_source_remove (menu->timeout_id);
+	  menu->timeout_id = 0;
+	  menu->scrolling_direction = 0;
+	}
+    }
+  
+  x = border_width + widget->style->xthickness;
+  y = border_width + widget->style->ythickness;
+  
+  if (menu->upper_arrow_visible)
+    y += MENU_SCROLL_ARROW_HEIGHT;
+  
+  gdk_window_move_resize (menu->view_window,
+			  x,
+			  y,
+			  view_width,
+			  view_height);
+
+  menu->first_item = position;
+}
+
+static void
+gtk_menu_select_item (GtkMenuShell  *menu_shell,
+		      GtkWidget     *menu_item)
+{
+  GtkMenu *menu;
+  GtkWidget *child;
+  GList *children;
+  GtkRequisition child_requisition;
+  gint child_offset, child_height;
+  gint width, height;
+  gint x, y;
+  guint pos;
+  gint arrow_height;
+  
+  g_return_if_fail (menu_shell != NULL);
+  g_return_if_fail (GTK_IS_MENU (menu_shell));
+
+  menu = GTK_MENU (menu_shell);
+
+  /* We need to check if the selected item fully visible.
+   * If not we need to scroll the menu so that it becomes fully
+   * visible.
+   */
+
+  pos = 0;
+  child = NULL;
+  child_offset = 0;
+  child_height = 0;
+  children = menu_shell->children;
+  while (children)
+    {
+      child = children->data;
+      children = children->next;
+      
+      if (GTK_WIDGET_VISIBLE (child))
+	{
+	  gtk_widget_size_request (child, &child_requisition);
+	  child_offset += child_height;
+	  child_height = child_requisition.height;
+	}
+      
+      if (child == menu_item)
+	break;
+
+      pos++;
+    }
+
+  if (child == menu_item)
+    {
+      gdk_window_get_position (menu->item_window, &x, &y);
+      y = -y;
+      gdk_window_get_size (GTK_WIDGET (menu)->window, &width, &height);
+
+      if (child_offset < y)
+	gtk_menu_scroll_to (menu, pos);
+      else
+	{
+	  arrow_height = 0;
+	  if (menu->upper_arrow_visible)
+	    arrow_height += MENU_SCROLL_ARROW_HEIGHT;
+	  if (menu->lower_arrow_visible)
+	    arrow_height += MENU_SCROLL_ARROW_HEIGHT;
+	  if (child_offset + child_height > y + height - arrow_height)
+	    {
+	      pos = 0;
+	      y = 0;
+	      children = menu_shell->children;
+	      while (children)
+		{
+		  child = children->data;
+		  children = children->next;
+		  
+		  if (GTK_WIDGET_VISIBLE (child))
+		    {
+		      arrow_height = 0;
+		      if (pos > 0)
+			arrow_height += MENU_SCROLL_ARROW_HEIGHT;
+		      if (children)
+			arrow_height += MENU_SCROLL_ARROW_HEIGHT;
+		      if (y + height >= child_offset + child_height + arrow_height) 
+			break;
+		      gtk_widget_size_request (child, &child_requisition);
+		      y += child_requisition.height;
+		    }
+		  
+		  pos++;
+		}
+	      
+	      
+	      gtk_menu_scroll_to (menu, pos);
+	    }
+	}    
+      
+    }
+
+  GTK_MENU_SHELL_CLASS (parent_class)->select_item (menu_shell, menu_item);
+}
+
+
 /* Reparent the menu, taking care of the refcounting
  */
 static void 
@@ -1477,7 +2140,6 @@ gtk_menu_reparent (GtkMenu      *menu, 
     }
   else
     gtk_widget_reparent (GTK_WIDGET (menu), new_parent);
-  gtk_widget_set_usize (new_parent, -1, -1);
   
   if (was_floating)
     GTK_OBJECT_SET_FLAGS (object, GTK_FLOATING);
Index: gtkmenu.h
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenu.h,v
retrieving revision 1.22
diff -u -p -r1.22 gtkmenu.h
--- gtkmenu.h	2000/09/02 02:43:50	1.22
+++ gtkmenu.h	2000/10/10 15:36:27
@@ -37,7 +37,6 @@
 extern "C" {
 #endif /* __cplusplus */
 
-
 #define GTK_TYPE_MENU			(gtk_menu_get_type ())
 #define GTK_MENU(obj)			(GTK_CHECK_CAST ((obj), GTK_TYPE_MENU, GtkMenu))
 #define GTK_MENU_CLASS(klass)		(GTK_CHECK_CLASS_CAST ((klass), GTK_TYPE_MENU, GtkMenuClass))
@@ -52,6 +51,7 @@ typedef struct _GtkMenuClass  GtkMenuCla
 typedef void (*GtkMenuPositionFunc) (GtkMenu   *menu,
 				     gint      *x,
 				     gint      *y,
+				     gint      *scroll_offset,
 				     gpointer	user_data);
 typedef void (*GtkMenuDetachFunc)   (GtkWidget *attach_widget,
 				     GtkMenu   *menu);
@@ -62,7 +62,7 @@ struct _GtkMenu
   
   GtkWidget *parent_menu_item;
   GtkWidget *old_active_menu_item;
-  
+
   GtkAccelGroup *accel_group;
   GtkMenuPositionFunc position_func;
   gpointer position_func_data;
@@ -74,6 +74,13 @@ struct _GtkMenu
   GtkWidget *toplevel;
   GtkWidget *tearoff_window;
 
+  GdkWindow *view_window;
+  GdkWindow *item_window;
+
+  guint first_item;
+  gint scrolling_direction;
+  guint timeout_id;
+  
   /* When a submenu of this menu is popped up, motion in this
    * region is ignored
    */
@@ -82,6 +89,12 @@ struct _GtkMenu
 
   guint needs_destruction_ref_count : 1;
   guint torn_off : 1;
+
+  guint upper_arrow_visible : 1;
+  guint lower_arrow_visible : 1;
+  guint upper_arrow_prelight : 1;
+  guint lower_arrow_prelight : 1;
+
 };
 
 struct _GtkMenuClass
Index: gtkmenuitem.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenuitem.c,v
retrieving revision 1.41
diff -u -p -r1.41 gtkmenuitem.c
--- gtkmenuitem.c	2000/07/26 11:32:45	1.41
+++ gtkmenuitem.c	2000/10/10 15:36:27
@@ -67,6 +67,7 @@ static void gtk_menu_item_popup_submenu 
 static void gtk_menu_item_position_menu  (GtkMenu          *menu,
 					  gint             *x,
 					  gint             *y,
+					  gint             *scroll_offset,
 					  gpointer          user_data);
 static void gtk_menu_item_show_all       (GtkWidget        *widget);
 static void gtk_menu_item_hide_all       (GtkWidget        *widget);
@@ -668,6 +669,7 @@ static void
 gtk_menu_item_position_menu (GtkMenu  *menu,
 			     gint     *x,
 			     gint     *y,
+			     gint     *scroll_offset,
 			     gpointer  user_data)
 {
   GtkMenuItem *menu_item;
@@ -702,9 +704,10 @@ gtk_menu_item_position_menu (GtkMenu  *m
 	ty += GTK_WIDGET (menu_item)->allocation.height;
       else if ((ty - theight) >= 0)
 	ty -= theight;
-      else
+      else if (screen_height - (ty + GTK_WIDGET (menu_item)->allocation.height) > ty)
 	ty += GTK_WIDGET (menu_item)->allocation.height;
-
+      else
+	ty -= theight;
       break;
 
     case GTK_LEFT_RIGHT:
@@ -738,14 +741,16 @@ gtk_menu_item_position_menu (GtkMenu  *m
 
       ty += GTK_WIDGET (menu_item)->allocation.height / 4;
 
+      /* If the height of the menu doesn't fit we move it upward. */
+      ty = CLAMP (ty, 0, MAX (0, screen_height - theight));
       break;
     }
 
-  /* If we have negative, tx, ty here it is because we can't get
-   * the menu all the way on screen. Favor the upper-left portion.
+  /* If we have negative, tx, here it is because we can't get
+   * the menu all the way on screen. Favor the left portion.
    */
   *x = CLAMP (tx, 0, MAX (0, screen_width - twidth));
-  *y = CLAMP (ty, 0, MAX (0, screen_height - theight));
+  *y = ty;
 }
 
 void
Index: gtkmenushell.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenushell.c,v
retrieving revision 1.33
diff -u -p -r1.33 gtkmenushell.c
--- gtkmenushell.c	2000/07/26 11:32:45	1.33
+++ gtkmenushell.c	2000/10/10 15:36:27
@@ -135,6 +135,8 @@ static gint gtk_menu_shell_is_item      
 static GtkWidget *gtk_menu_shell_get_item    (GtkMenuShell      *menu_shell,
 					      GdkEvent          *event);
 static GtkType    gtk_menu_shell_child_type  (GtkContainer      *container);
+static void gtk_menu_shell_real_select_item  (GtkMenuShell      *menu_shell,
+					      GtkWidget         *menu_item);
 static void gtk_menu_shell_select_submenu_first (GtkMenuShell   *menu_shell); 
 
 static void gtk_real_menu_shell_move_current (GtkMenuShell      *menu_shell,
@@ -246,6 +248,7 @@ gtk_menu_shell_class_init (GtkMenuShellC
   klass->move_current = gtk_real_menu_shell_move_current;
   klass->activate_current = gtk_real_menu_shell_activate_current;
   klass->cancel = gtk_real_menu_shell_cancel;
+  klass->select_item = gtk_menu_shell_real_select_item;
 
   binding_set = gtk_binding_set_by_class (klass);
   gtk_binding_entry_add_signal (binding_set,
@@ -775,6 +778,24 @@ gtk_menu_shell_get_item (GtkMenuShell *m
 void
 gtk_menu_shell_select_item (GtkMenuShell *menu_shell,
 			    GtkWidget    *menu_item)
+{
+  GtkMenuShellClass *class;
+
+  g_return_if_fail (menu_shell != NULL);
+  g_return_if_fail (GTK_IS_MENU_SHELL (menu_shell));
+  g_return_if_fail (menu_item != NULL);
+  g_return_if_fail (GTK_IS_MENU_ITEM (menu_item));
+
+  class = GTK_MENU_SHELL_GET_CLASS (menu_shell);
+
+  if (class->select_item)
+    class->select_item (menu_shell, menu_item);
+}
+
+
+static void
+gtk_menu_shell_real_select_item (GtkMenuShell *menu_shell,
+				 GtkWidget    *menu_item)
 {
   g_return_if_fail (menu_shell != NULL);
   g_return_if_fail (GTK_IS_MENU_SHELL (menu_shell));
Index: gtkmenushell.h
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmenushell.h,v
retrieving revision 1.10
diff -u -p -r1.10 gtkmenushell.h
--- gtkmenushell.h	2000/08/30 00:33:37	1.10
+++ gtkmenushell.h	2000/10/10 15:36:27
@@ -81,6 +81,8 @@ struct _GtkMenuShellClass
   void (*activate_current) (GtkMenuShell *menu_shell,
 			    gboolean      force_hide);
   void (*cancel)           (GtkMenuShell *menu_shell);
+  void (*select_item)      (GtkMenuShell *menu_shell,
+			    GtkWidget    *menu_item);
 };
 
 
Index: gtkoptionmenu.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkoptionmenu.c,v
retrieving revision 1.32
diff -u -p -r1.32 gtkoptionmenu.c
--- gtkoptionmenu.c	2000/07/26 11:32:45	1.32
+++ gtkoptionmenu.c	2000/10/10 15:36:27
@@ -65,6 +65,7 @@ static void gtk_option_menu_calc_size   
 static void gtk_option_menu_position        (GtkMenu            *menu,
 					     gint               *x,
 					     gint               *y,
+					     gint               *scroll_offset,
 					     gpointer            user_data);
 static void gtk_option_menu_show_all        (GtkWidget          *widget);
 static void gtk_option_menu_hide_all        (GtkWidget          *widget);
@@ -650,6 +651,7 @@ static void
 gtk_option_menu_position (GtkMenu  *menu,
 			  gint     *x,
 			  gint     *y,
+			  gint     *scroll_offset,
 			  gpointer  user_data)
 {
   GtkOptionMenu *option_menu;
@@ -657,9 +659,7 @@ gtk_option_menu_position (GtkMenu  *menu
   GtkWidget *child;
   GtkRequisition requisition;
   GList *children;
-  gint shift_menu;
   gint screen_width;
-  gint screen_height;
   gint menu_xpos;
   gint menu_ypos;
   gint width;
@@ -702,34 +702,23 @@ gtk_option_menu_position (GtkMenu  *menu
       children = children->next;
     }
 
+      
   screen_width = gdk_screen_width ();
-  screen_height = gdk_screen_height ();
-
-  shift_menu = FALSE;
-  if (menu_ypos < 0)
-    {
-      menu_ypos = 0;
-      shift_menu = TRUE;
-    }
-  else if ((menu_ypos + height) > screen_height)
-    {
-      menu_ypos -= ((menu_ypos + height) - screen_height);
-      shift_menu = TRUE;
-    }
-
-  if (shift_menu)
-    {
-      if ((menu_xpos + GTK_WIDGET (option_menu)->allocation.width + width) <= screen_width)
-	menu_xpos += GTK_WIDGET (option_menu)->allocation.width;
-      else
-	menu_xpos -= width;
-    }
-
+  
   if (menu_xpos < 0)
     menu_xpos = 0;
   else if ((menu_xpos + width) > screen_width)
     menu_xpos -= ((menu_xpos + width) - screen_width);
 
+  if (menu_ypos < 0)
+    {
+      *scroll_offset = -menu_ypos;
+      menu_ypos = 0;
+      
+    }
+  else
+    *scroll_offset = 0;
+  
   *x = menu_xpos;
   *y = menu_ypos;
 }






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