Scrolled window keyboard navigation



The following patch is an attempt to implement the keynav
suggestions for GtkScrolledWin in:

 http://bugzilla.gnome.org/show_bug.cgi?id=63480

And 

 http://developer.gnome.org/projects/gap/keynav/gtk_containers.html

It adds the following keybindings:

 Arrow keys:               scroll scrolled window
 PageUp/PageDown:          page up and down
 Control-PageUp/PageDown   page left and right
 Home/End                  all the way left, all the way right
 Control-Home/End          all the way up, all the way down

[ Note, Control-Home/End Home/End are reversed from the
  keynav table. I think this is more consistent with the
  way that text widgets works ]

 Control-[Shift]-Tab       focus out of the scrolled window

This bindings take effect if the key press is not handled by the
focus widget or any widget between the focus widget and and the
scrolled window.

The patch also makes the scrolled window itself a possible focus
location, but only if no widget inside the scrolled window can
focus.

Concerns:

 * The patch currently doesn't draw any focus indication if the
   focus window itself is focused. I don't want to reserve the
   space for a focus rectangle around the scrolled window since
   the scrolled window will be focused so rarely. (Never for most
   scrolled windows.)  My idea currently is that a good focus
   indication might be to draw focus indicator rectangles within
   the thumbs of the two scrollbars. This would require some
   scrollbar/scrolled-window private API addition.

 * If the scrolled window contains multiple focusable widgets in
   it, then scrolling the scrolled window wwith these bindings
   can result in the focus widget being scrolled out of the
   visible area. (This is poor UI design, of course.) The
   scrolled window example in testgtk demonstrates this issue
   well.
  
 * Since the arrow keys are bound at the scrolled window level,
   they don't get up to the toplevel, so directional focusing
   with arrow keys within the scrolled window won't work. This
   includes things like focusing between the tabs of a notebook,
   or (more important here) the headers of a GtkTreeView. Fixing
   this to make these arrows not be overriden by the scrolled
   window would require using something other than the standard
   focus() method to drive motion by arrow keys in these
   contexts.

   Control-arrows can be used for directional focusing, but I
   think it might be better to reverse things and use
   control-arrows to scroll the scrolled window and leave the
   arrows working normally for widgets within the widget.

   (It's also possible that the scrolled windows should
   directionally focus out of the entire scrolled window in
   analogy to Control-Tab, but that's probably less useful than
   the other meanings here.)

Regards,
                                        Owen

Index: ChangeLog
===================================================================
RCS file: /cvs/gnome/gtk+/ChangeLog,v
retrieving revision 1.2964
diff -u -p -r1.2964 ChangeLog
--- ChangeLog	2002/02/08 02:38:41	1.2964
+++ ChangeLog	2002/02/08 03:44:12
@@ -1,3 +1,30 @@
+Thu Feb  7 21:28:48 2002  Owen Taylor  <otaylor redhat com>
+
+	* gtk/gtkscrolledwindow.c (gtk_scrolled_window_focus): Focus
+	the widget itself if no child can be focused.
+
+	* gtk/gtkscrolledwindow.[ch]: Add keybindings to scroll the 
+	window when focus is on a child, and to focus out of the
+	entire scrolled window.
+
Index: gtk/gtkscrolledwindow.c
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkscrolledwindow.c,v
retrieving revision 1.54
diff -u -p -r1.54 gtkscrolledwindow.c
--- gtk/gtkscrolledwindow.c	2001/07/19 19:49:01	1.54
+++ gtk/gtkscrolledwindow.c	2002/02/08 03:44:12
@@ -24,6 +24,9 @@
  * GTK+ at ftp://ftp.gtk.org/pub/gtk/. 
  */
 
+#include <gdk/gdkkeysyms.h>
+#include "gtkbindings.h"
+#include "gtkmarshalers.h"
 #include "gtkscrolledwindow.h"
 #include "gtksignal.h"
 #include "gtkintl.h"
@@ -79,6 +82,14 @@ enum {
   PROP_LAST
 };
 
+/* Signals */
+enum
+{
+  SCROLL_CHILD,
+  MOVE_FOCUS_OUT,
+  LAST_SIGNAL
+};
+
 static void gtk_scrolled_window_class_init         (GtkScrolledWindowClass *klass);
 static void gtk_scrolled_window_init               (GtkScrolledWindow      *scrolled_window);
 static void gtk_scrolled_window_destroy            (GtkObject              *object);
@@ -100,6 +111,8 @@ static void gtk_scrolled_window_size_all
 						    GtkAllocation          *allocation);
 static gint gtk_scrolled_window_scroll_event       (GtkWidget              *widget,
 						    GdkEventScroll         *event);
+static gint gtk_scrolled_window_focus              (GtkWidget              *widget,
+						    GtkDirectionType        direction);
 static void gtk_scrolled_window_add                (GtkContainer           *container,
 						    GtkWidget              *widget);
 static void gtk_scrolled_window_remove             (GtkContainer           *container,
@@ -108,6 +121,12 @@ static void gtk_scrolled_window_forall  
 						    gboolean		    include_internals,
 						    GtkCallback             callback,
 						    gpointer                callback_data);
+static void gtk_scrolled_window_scroll_child       (GtkScrolledWindow      *scrolled_window,
+						    GtkScrollType           scroll,
+						    gboolean                horizontal);
+static void gtk_scrolled_window_move_focus_out     (GtkScrolledWindow      *scrolled_window,
+						    GtkDirectionType        direction_type);
+
 static void gtk_scrolled_window_relative_allocation(GtkWidget              *widget,
 						    GtkAllocation          *allocation);
 static void gtk_scrolled_window_adjustment_changed (GtkAdjustment          *adjustment,
@@ -115,6 +134,7 @@ static void gtk_scrolled_window_adjustme
 
 static GtkContainerClass *parent_class = NULL;
 
+static guint signals[LAST_SIGNAL] = {0};
 
 GtkType
 gtk_scrolled_window_get_type (void)
@@ -142,12 +162,48 @@ gtk_scrolled_window_get_type (void)
 }
 
 static void
+add_scroll_binding (GtkBindingSet  *binding_set,
+		    guint           keyval,
+		    GdkModifierType mask,
+		    GtkScrollType   scroll,
+		    gboolean        horizontal)
+{
+  guint keypad_keyval = keyval - GDK_Left + GDK_KP_Left;
+  
+  gtk_binding_entry_add_signal (binding_set, keyval, mask,
+                                "scroll_child", 2,
+                                GTK_TYPE_SCROLL_TYPE, scroll,
+				G_TYPE_BOOLEAN, horizontal);
+  gtk_binding_entry_add_signal (binding_set, keypad_keyval, mask,
+                                "scroll_child", 2,
+                                GTK_TYPE_SCROLL_TYPE, scroll,
+				G_TYPE_BOOLEAN, horizontal);
+}
+
+static void
+add_tab_bindings (GtkBindingSet    *binding_set,
+		  GdkModifierType   modifiers,
+		  GtkDirectionType  direction)
+{
+  gtk_binding_entry_add_signal (binding_set, GDK_Tab, modifiers,
+                                "move_focus_out", 1,
+                                GTK_TYPE_DIRECTION_TYPE, direction);
+  gtk_binding_entry_add_signal (binding_set, GDK_KP_Tab, modifiers,
+                                "move_focus_out", 1,
+                                GTK_TYPE_DIRECTION_TYPE, direction);
+  gtk_binding_entry_add_signal (binding_set, GDK_ISO_Left_Tab, modifiers,
+                                "move_focus_out", 1,
+                                GTK_TYPE_DIRECTION_TYPE, direction);
+}
+
+static void
 gtk_scrolled_window_class_init (GtkScrolledWindowClass *class)
 {
   GObjectClass *gobject_class = G_OBJECT_CLASS (class);
   GtkObjectClass *object_class;
   GtkWidgetClass *widget_class;
   GtkContainerClass *container_class;
+  GtkBindingSet *binding_set;
 
   object_class = (GtkObjectClass*) class;
   widget_class = (GtkWidgetClass*) class;
@@ -164,6 +220,7 @@ gtk_scrolled_window_class_init (GtkScrol
   widget_class->size_request = gtk_scrolled_window_size_request;
   widget_class->size_allocate = gtk_scrolled_window_size_allocate;
   widget_class->scroll_event = gtk_scrolled_window_scroll_event;
+  widget_class->focus = gtk_scrolled_window_focus;
 
   container_class->add = gtk_scrolled_window_add;
   container_class->remove = gtk_scrolled_window_remove;
@@ -171,6 +228,9 @@ gtk_scrolled_window_class_init (GtkScrol
 
   class->scrollbar_spacing = DEFAULT_SCROLLBAR_SPACING;
 
+  class->scroll_child = gtk_scrolled_window_scroll_child;
+  class->move_focus_out = gtk_scrolled_window_move_focus_out;
+  
   g_object_class_install_property (gobject_class,
 				   PROP_HADJUSTMENT,
 				   g_param_spec_object ("hadjustment",
@@ -218,12 +278,57 @@ gtk_scrolled_window_class_init (GtkScrol
 						      GTK_TYPE_SHADOW_TYPE,
 						      GTK_SHADOW_NONE,
                                                       G_PARAM_READABLE | G_PARAM_WRITABLE));
+
+  signals[SCROLL_CHILD] =
+    g_signal_new ("scroll_child",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (GtkScrolledWindowClass, scroll_child),
+                  NULL, NULL,
+                  _gtk_marshal_VOID__ENUM_BOOLEAN,
+                  G_TYPE_NONE, 2,
+                  GTK_TYPE_SCROLL_TYPE,
+		  G_TYPE_BOOLEAN);
+  signals[MOVE_FOCUS_OUT] =
+    g_signal_new ("move_focus_out",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (GtkScrolledWindowClass, move_focus_out),
+                  NULL, NULL,
+                  _gtk_marshal_VOID__ENUM,
+                  G_TYPE_NONE, 1,
+                  GTK_TYPE_DIRECTION_TYPE);
+  
+  binding_set = gtk_binding_set_by_class (class);
+
+  add_scroll_binding (binding_set, GDK_Left,  0, GTK_SCROLL_STEP_BACKWARD, TRUE);
+  add_scroll_binding (binding_set, GDK_Right, 0, GTK_SCROLL_STEP_FORWARD,  TRUE);
+  add_scroll_binding (binding_set, GDK_Up,    0, GTK_SCROLL_STEP_BACKWARD, FALSE);
+  add_scroll_binding (binding_set, GDK_Down,  0, GTK_SCROLL_STEP_FORWARD,  FALSE);
+
+  add_scroll_binding (binding_set, GDK_Page_Up,   GDK_CONTROL_MASK, GTK_SCROLL_PAGE_BACKWARD, TRUE);
+  add_scroll_binding (binding_set, GDK_Page_Down, GDK_CONTROL_MASK, GTK_SCROLL_PAGE_FORWARD,  TRUE);
+  add_scroll_binding (binding_set, GDK_Page_Up,   0,                GTK_SCROLL_PAGE_BACKWARD, FALSE);
+  add_scroll_binding (binding_set, GDK_Page_Down, 0,                GTK_SCROLL_PAGE_FORWARD,  FALSE);
+
+  add_scroll_binding (binding_set, GDK_Page_Up,   GDK_CONTROL_MASK, GTK_SCROLL_PAGE_BACKWARD, TRUE);
+  add_scroll_binding (binding_set, GDK_Page_Down, GDK_CONTROL_MASK, GTK_SCROLL_PAGE_FORWARD,  TRUE);
+  add_scroll_binding (binding_set, GDK_Page_Up,   0,                GTK_SCROLL_PAGE_BACKWARD, FALSE);
+  add_scroll_binding (binding_set, GDK_Page_Down, 0,                GTK_SCROLL_PAGE_FORWARD,  FALSE);
+
+  add_scroll_binding (binding_set, GDK_Home, 0,                GTK_SCROLL_START, TRUE);
+  add_scroll_binding (binding_set, GDK_End,  0,                GTK_SCROLL_END,   TRUE);
+  add_scroll_binding (binding_set, GDK_Home, GDK_CONTROL_MASK, GTK_SCROLL_START, FALSE);
+  add_scroll_binding (binding_set, GDK_End,  GDK_CONTROL_MASK, GTK_SCROLL_END,   FALSE);
+
+  add_tab_bindings (binding_set, GDK_CONTROL_MASK, GTK_DIR_TAB_FORWARD);
+  add_tab_bindings (binding_set, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_DIR_TAB_BACKWARD);
 }
 
 static void
 gtk_scrolled_window_init (GtkScrolledWindow *scrolled_window)
 {
-  GTK_WIDGET_SET_FLAGS (scrolled_window, GTK_NO_WINDOW);
+  GTK_WIDGET_SET_FLAGS (scrolled_window, GTK_NO_WINDOW | GTK_CAN_FOCUS);
 
   gtk_container_set_resize_mode (GTK_CONTAINER (scrolled_window), GTK_RESIZE_QUEUE);
 
@@ -233,6 +338,7 @@ gtk_scrolled_window_init (GtkScrolledWin
   scrolled_window->vscrollbar_policy = GTK_POLICY_ALWAYS;
   scrolled_window->hscrollbar_visible = FALSE;
   scrolled_window->vscrollbar_visible = FALSE;
+  scrolled_window->focus_out = FALSE;
   scrolled_window->window_placement = GTK_CORNER_TOP_LEFT;
   
 }
@@ -683,6 +789,127 @@ gtk_scrolled_window_forall (GtkContainer
 }
 
 static void
+gtk_scrolled_window_scroll_child (GtkScrolledWindow *scrolled_window,
+				  GtkScrollType      scroll,
+				  gboolean           horizontal)
+{
+  GtkAdjustment *adjustment = NULL;
+  
+  switch (scroll)
+    {
+    case GTK_SCROLL_STEP_UP:
+      scroll = GTK_SCROLL_STEP_BACKWARD;
+      horizontal = FALSE;
+      break;
+    case GTK_SCROLL_STEP_DOWN:
+      scroll = GTK_SCROLL_STEP_FORWARD;
+      horizontal = FALSE;
+      break;
+    case GTK_SCROLL_STEP_LEFT:
+      scroll = GTK_SCROLL_STEP_BACKWARD;
+      horizontal = TRUE;
+      break;
+    case GTK_SCROLL_STEP_RIGHT:
+      scroll = GTK_SCROLL_STEP_FORWARD;
+      horizontal = TRUE;
+      break;
+    case GTK_SCROLL_PAGE_UP:
+      scroll = GTK_SCROLL_PAGE_BACKWARD;
+      horizontal = FALSE;
+      break;
+    case GTK_SCROLL_PAGE_DOWN:
+      scroll = GTK_SCROLL_PAGE_FORWARD;
+      horizontal = FALSE;
+      break;
+    case GTK_SCROLL_PAGE_LEFT:
+      scroll = GTK_SCROLL_STEP_BACKWARD;
+      horizontal = TRUE;
+      break;
+    case GTK_SCROLL_PAGE_RIGHT:
+      scroll = GTK_SCROLL_STEP_FORWARD;
+      horizontal = TRUE;
+      break;
+    case GTK_SCROLL_STEP_BACKWARD:
+    case GTK_SCROLL_STEP_FORWARD:
+    case GTK_SCROLL_PAGE_BACKWARD:
+    case GTK_SCROLL_PAGE_FORWARD:
+    case GTK_SCROLL_START:
+    case GTK_SCROLL_END:
+      break;
+    default:
+      g_warning ("Invalid scroll type %d for GtkSpinButton::change-value", scroll);
+      return;
+    }
+
+  if (horizontal)
+    {
+      if (scrolled_window->hscrollbar)
+	adjustment = gtk_range_get_adjustment (GTK_RANGE (scrolled_window->hscrollbar));
+    }
+  else
+    {
+      if (scrolled_window->vscrollbar)
+	adjustment = gtk_range_get_adjustment (GTK_RANGE (scrolled_window->vscrollbar));
+    }
+
+  if (adjustment)
+    {
+      gdouble value = adjustment->value;
+      
+      switch (scroll)
+	{
+	case GTK_SCROLL_STEP_FORWARD:
+	  value += adjustment->step_increment;
+	  break;
+	case GTK_SCROLL_STEP_BACKWARD:
+	  value -= adjustment->step_increment;
+	  break;
+	case GTK_SCROLL_PAGE_FORWARD:
+	  value += adjustment->page_increment;
+	  break;
+	case GTK_SCROLL_PAGE_BACKWARD:
+	  value -= adjustment->page_increment;
+	  break;
+	case GTK_SCROLL_START:
+	  value = adjustment->lower;
+	  break;
+	case GTK_SCROLL_END:
+	  value = adjustment->upper;
+	  break;
+	default:
+	  g_assert_not_reached ();
+	  break;
+	}
+
+      value = CLAMP (value, adjustment->lower, adjustment->upper - adjustment->page_size);
+      
+      gtk_adjustment_set_value (adjustment, value);
+    }
+}
+
+static void
+gtk_scrolled_window_move_focus_out (GtkScrolledWindow *scrolled_window,
+				    GtkDirectionType   direction_type)
+{
+  GtkWidget *toplevel;
+  
+  /* Focus out of the scrolled window entirely. We do this by setting
+   * a flag, then propagating the focus motion to the notebook.
+   */
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (scrolled_window));
+  if (!GTK_WIDGET_TOPLEVEL (toplevel))
+    return;
+
+  g_object_ref (scrolled_window);
+  
+  scrolled_window->focus_out = TRUE;
+  g_signal_emit_by_name (G_OBJECT (toplevel), "move_focus", direction_type);
+  scrolled_window->focus_out = FALSE;
+  
+  g_object_unref (scrolled_window);
+}
+
+static void
 gtk_scrolled_window_size_request (GtkWidget      *widget,
 				  GtkRequisition *requisition)
 {
@@ -1000,6 +1227,31 @@ gtk_scrolled_window_scroll_event (GtkWid
     }
 
   return FALSE;
+}
+
+static gint
+gtk_scrolled_window_focus (GtkWidget        *widget,
+			   GtkDirectionType  direction)
+{
+  GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW (widget);
+  
+  if (scrolled_window->focus_out)
+    {
+      scrolled_window->focus_out = FALSE; /* Clear this to catch the wrap-around case */
+      return FALSE;
+    }
+  
+  if (gtk_widget_is_focus (widget))
+    return FALSE;
+
+  if (GTK_BIN (widget)->child)
+    {
+      if (gtk_widget_child_focus (GTK_BIN (widget)->child, direction))
+	return TRUE;
+    }
+
+  gtk_widget_grab_focus (widget);
+  return TRUE;
 }
 
 static void
Index: gtk/gtkscrolledwindow.h
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkscrolledwindow.h,v
retrieving revision 1.18
diff -u -p -r1.18 gtkscrolledwindow.h
--- gtk/gtkscrolledwindow.h	2001/09/08 06:24:46	1.18
+++ gtk/gtkscrolledwindow.h	2002/02/08 03:44:12
@@ -63,6 +63,7 @@ struct _GtkScrolledWindow
   guint hscrollbar_visible     : 1;
   guint vscrollbar_visible     : 1;
   guint window_placement       : 2;
+  guint focus_out              : 1;	/* Flag used by ::move-focus-out implementation */
 
   guint16 shadow_type;
 };
@@ -72,6 +73,20 @@ struct _GtkScrolledWindowClass
   GtkBinClass parent_class;
   
   gint scrollbar_spacing;
+
+  /* Action signals for keybindings. Do not connect to these signals
+   */
+
+  /* Unfortunately, GtkScrollType is deficient in that there is
+   * no horizontal/vertical variants for GTK_SCROLL_START/END,
+   * so we have to add an additional boolean flag.
+   */
+  void (*scroll_child) (GtkScrolledWindow *scrolled_window,
+			GtkScrollType      scroll,
+			gboolean           horizontal);
+
+  void (* move_focus_out) (GtkScrolledWindow *scrolled_window,
+			   GtkDirectionType   direction);
 };
 
 
Index: gtk/gtkmarshalers.list
===================================================================
RCS file: /cvs/gnome/gtk+/gtk/gtkmarshalers.list,v
retrieving revision 1.45
diff -u -p -r1.45 gtkmarshalers.list
--- gtk/gtkmarshalers.list	2002/01/17 22:38:18	1.45
+++ gtk/gtkmarshalers.list	2002/02/08 03:44:12
@@ -52,6 +52,7 @@ VOID:BOXED,UINT
 VOID:BOXED,UINT,FLAGS
 VOID:BOXED,UINT,UINT
 VOID:ENUM
+VOID:ENUM,BOOLEAN
 VOID:ENUM,ENUM
 VOID:ENUM,FLOAT
 VOID:ENUM,FLOAT,BOOLEAN



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