From 7f374a74bae63c9c1011c2a5a9c99de4012e7c61 Mon Sep 17 00:00:00 2001 From: Michael Natterer Date: Thu, 16 Nov 2006 12:56:30 +0000 Subject: [PATCH] Add new infrastructure for notifications of failed keyboard navigation and 2006-11-16 Michael Natterer Add new infrastructure for notifications of failed keyboard navigation and navigation with restricted set of keys. The patch handles configurable beeping, navigating the GUI with cursor keys only (as in phone environments), and configurable wrap-around. Fixes bugs #322640, #70986, #318827, #334726, #334742 and #309291. * gtk/gtksettings.c: added properties gtk-keynav-cursor-only, gtk-keynav-wrap-around and gtk-error-bell. * gtk/gtkwidget.[ch]: added new signal "keynav-failed" and public API to emit it. Added New function gtk_widget_error_bell() which looks at the gtk-error-bell setting and calls gdk_window_beep() accordingly. * gtk/gtk.symbols: add the new widget symbols. * gtk/gtkcellrendereraccel.c * gtk/gtkimcontextsimple.c * gtk/gtkmenu.c * gtk/gtknotebook.c: use gtk_widget_error_bell() or look at the gtk-error-bell setting instead of calling gdk_display_beep() unconditionally. * gtk/gtkcombobox.c * gtk/gtkentry.c * gtk/gtkiconview.c * gtk/gtklabel.c * gtk/gtkmenushell.c * gtk/gtkspinbutton.c * gtk/gtktextview.c * gtk/gtktreeview.c: call gtk_widget_error_bell() on failed keynav. * gtk/gtkentry.c * gtk/gtklabel.c * gtk/gtkrange.c * gtk/gtktextview.c: consult gtk_widget_keynav_failed() on failed cursor navigation and leave the widget if it returns FALSE. * gtk/gtkmenushell.c * gtk/gtknotebook.c: only wrap around if gtk-keynav-wrap-around is TRUE. * gtk/gtkradiobutton.c: ask gtk_widget_keynav_failed() to decide whether to to wrap-around, and don't select active items on cursor navigation if gtk-keynav-cursor-only is TRUE. Should look at gtk-keynav-wrap-around too, will look into that. --- ChangeLog | 51 +++++++++++++ gtk/gtk.symbols | 2 + gtk/gtkcellrendereraccel.c | 2 +- gtk/gtkcombobox.c | 45 ++++++------ gtk/gtkentry.c | 73 ++++++++++++++++--- gtk/gtkiconview.c | 16 ++++- gtk/gtkimcontextsimple.c | 39 ++++++++-- gtk/gtklabel.c | 35 +++++++-- gtk/gtkmenu.c | 4 +- gtk/gtkmenushell.c | 29 ++++++-- gtk/gtknotebook.c | 40 +++++++++-- gtk/gtkradiobutton.c | 17 ++++- gtk/gtkrange.c | 36 ++++++++++ gtk/gtksettings.c | 63 +++++++++++++++- gtk/gtkspinbutton.c | 5 ++ gtk/gtktextview.c | 141 ++++++++++++++++++++++++++---------- gtk/gtktreeview.c | 129 +++++++++++++++++++++++++-------- gtk/gtkwidget.c | 143 ++++++++++++++++++++++++++++++++++++- gtk/gtkwidget.h | 5 +- 19 files changed, 743 insertions(+), 132 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0a95d7fdf7..129b653200 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,54 @@ +2006-11-16 Michael Natterer + + Add new infrastructure for notifications of failed keyboard + navigation and navigation with restricted set of keys. + + The patch handles configurable beeping, navigating the GUI with + cursor keys only (as in phone environments), and configurable + wrap-around. Fixes bugs #322640, #70986, #318827, #334726, #334742 + and #309291. + + * gtk/gtksettings.c: added properties gtk-keynav-cursor-only, + gtk-keynav-wrap-around and gtk-error-bell. + + * gtk/gtkwidget.[ch]: added new signal "keynav-failed" and public + API to emit it. Added New function gtk_widget_error_bell() which + looks at the gtk-error-bell setting and calls gdk_window_beep() + accordingly. + + * gtk/gtk.symbols: add the new widget symbols. + + * gtk/gtkcellrendereraccel.c + * gtk/gtkimcontextsimple.c + * gtk/gtkmenu.c + * gtk/gtknotebook.c: use gtk_widget_error_bell() or look at the + gtk-error-bell setting instead of calling gdk_display_beep() + unconditionally. + + * gtk/gtkcombobox.c + * gtk/gtkentry.c + * gtk/gtkiconview.c + * gtk/gtklabel.c + * gtk/gtkmenushell.c + * gtk/gtkspinbutton.c + * gtk/gtktextview.c + * gtk/gtktreeview.c: call gtk_widget_error_bell() on failed keynav. + + * gtk/gtkentry.c + * gtk/gtklabel.c + * gtk/gtkrange.c + * gtk/gtktextview.c: consult gtk_widget_keynav_failed() on failed + cursor navigation and leave the widget if it returns FALSE. + + * gtk/gtkmenushell.c + * gtk/gtknotebook.c: only wrap around if gtk-keynav-wrap-around + is TRUE. + + * gtk/gtkradiobutton.c: ask gtk_widget_keynav_failed() to decide + whether to to wrap-around, and don't select active items on cursor + navigation if gtk-keynav-cursor-only is TRUE. Should look at + gtk-keynav-wrap-around too, will look into that. + 2006-11-16 Emmanuele Bassi * gtk/gtkrecentmanager.c: diff --git a/gtk/gtk.symbols b/gtk/gtk.symbols index 61343e4a08..939370a65d 100644 --- a/gtk/gtk.symbols +++ b/gtk/gtk.symbols @@ -4470,6 +4470,7 @@ gtk_widget_create_pango_layout gtk_widget_destroy gtk_widget_destroyed gtk_widget_ensure_style +gtk_widget_error_bell gtk_widget_event gtk_widget_freeze_child_notify gtk_widget_get_accessible @@ -4511,6 +4512,7 @@ gtk_widget_hide_on_delete gtk_widget_intersect gtk_widget_is_ancestor gtk_widget_is_focus +gtk_widget_keynav_failed gtk_widget_list_accel_closures gtk_widget_list_mnemonic_labels gtk_widget_map diff --git a/gtk/gtkcellrendereraccel.c b/gtk/gtkcellrendereraccel.c index 31b70c3e7e..9157fc2b60 100644 --- a/gtk/gtkcellrendereraccel.c +++ b/gtk/gtkcellrendereraccel.c @@ -456,7 +456,7 @@ grab_key_callback (GtkWidget *widget, { if (!gtk_accelerator_valid (accel_key, accel_mods)) { - gdk_display_beep (display); + gtk_widget_error_bell (widget); return TRUE; } diff --git a/gtk/gtkcombobox.c b/gtk/gtkcombobox.c index 1cf287fe0f..c1d2bdd946 100644 --- a/gtk/gtkcombobox.c +++ b/gtk/gtkcombobox.c @@ -4978,7 +4978,10 @@ gtk_combo_box_real_move_active (GtkComboBox *combo_box, gboolean found; if (!combo_box->priv->model) - return; + { + gtk_widget_error_bell (GTK_WIDGET (combo_box)); + return; + } active_iter = gtk_combo_box_get_active_iter (combo_box, &iter); @@ -5024,28 +5027,28 @@ gtk_combo_box_real_move_active (GtkComboBox *combo_box, return; } + if (found && active_iter) + { + GtkTreePath *old_path; + GtkTreePath *new_path; + + old_path = gtk_tree_model_get_path (combo_box->priv->model, &iter); + new_path = gtk_tree_model_get_path (combo_box->priv->model, &new_iter); + + if (gtk_tree_path_compare (old_path, new_path) == 0) + found = FALSE; + + gtk_tree_path_free (old_path); + gtk_tree_path_free (new_path); + } + if (found) { - if (active_iter) - { - GtkTreePath *old_path; - GtkTreePath *new_path; - - old_path = gtk_tree_model_get_path (combo_box->priv->model, &iter); - new_path = gtk_tree_model_get_path (combo_box->priv->model, &new_iter); - - if (gtk_tree_path_compare (old_path, new_path) == 0) - found = FALSE; - - gtk_tree_path_free (old_path); - gtk_tree_path_free (new_path); - } - - if (found) - { - gtk_combo_box_set_active_iter (combo_box, &new_iter); - return; - } + gtk_combo_box_set_active_iter (combo_box, &new_iter); + } + else + { + gtk_widget_error_bell (GTK_WIDGET (combo_box)); } } diff --git a/gtk/gtkentry.c b/gtk/gtkentry.c index 701c4347a4..30f96f1072 100644 --- a/gtk/gtkentry.c +++ b/gtk/gtkentry.c @@ -1709,12 +1709,18 @@ gtk_entry_button_press (GtkWidget *widget, return TRUE; } - else if (event->button == 2 && event->type == GDK_BUTTON_PRESS && entry->editable) + else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) { - priv->insert_pos = tmp_pos; - gtk_entry_paste (entry, GDK_SELECTION_PRIMARY); - - return TRUE; + if (entry->editable) + { + priv->insert_pos = tmp_pos; + gtk_entry_paste (entry, GDK_SELECTION_PRIMARY); + return TRUE; + } + else + { + gtk_widget_error_bell (widget); + } } else if (event->button == 3 && event->type == GDK_BUTTON_PRESS) { @@ -1976,6 +1982,9 @@ gtk_entry_key_press (GtkWidget *widget, */ return TRUE; + if (!entry->editable && event->length) + gtk_widget_error_bell (widget); + return FALSE; } @@ -2329,7 +2338,7 @@ gtk_entry_real_insert_text (GtkEditable *editable, n_chars = g_utf8_strlen (new_text, new_text_length); if (entry->text_max_length > 0 && n_chars + entry->text_length > entry->text_max_length) { - gdk_display_beep (gtk_widget_get_display (GTK_WIDGET (entry))); + gtk_widget_error_bell (GTK_WIDGET (entry)); n_chars = entry->text_max_length - entry->text_length; new_text_length = g_utf8_offset_to_pointer (new_text, n_chars) - new_text; } @@ -2535,7 +2544,6 @@ gtk_entry_move_cursor (GtkEntry *entry, new_pos = current_x < bound_x ? entry->current_pos : entry->selection_bound; else new_pos = current_x > bound_x ? entry->current_pos : entry->selection_bound; - break; } case GTK_MOVEMENT_LOGICAL_POSITIONS: @@ -2566,6 +2574,27 @@ gtk_entry_move_cursor (GtkEntry *entry, break; case GTK_MOVEMENT_VISUAL_POSITIONS: new_pos = gtk_entry_move_visually (entry, new_pos, count); + if (entry->current_pos == new_pos) + { + if (!extend_selection) + { + if (!gtk_widget_keynav_failed (GTK_WIDGET (entry), + count > 0 ? + GTK_DIR_RIGHT : GTK_DIR_LEFT)) + { + GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (entry)); + + if (toplevel) + gtk_widget_child_focus (toplevel, + count > 0 ? + GTK_DIR_RIGHT : GTK_DIR_LEFT); + } + } + else + { + gtk_widget_error_bell (GTK_WIDGET (entry)); + } + } break; case GTK_MOVEMENT_WORDS: while (count > 0) @@ -2578,11 +2607,15 @@ gtk_entry_move_cursor (GtkEntry *entry, new_pos = gtk_entry_move_backward_word (entry, new_pos, FALSE); count++; } + if (entry->current_pos == new_pos) + gtk_widget_error_bell (GTK_WIDGET (entry)); break; case GTK_MOVEMENT_DISPLAY_LINE_ENDS: case GTK_MOVEMENT_PARAGRAPH_ENDS: case GTK_MOVEMENT_BUFFER_ENDS: new_pos = count < 0 ? 0 : entry->text_length; + if (entry->current_pos == new_pos) + gtk_widget_error_bell (GTK_WIDGET (entry)); break; case GTK_MOVEMENT_DISPLAY_LINES: case GTK_MOVEMENT_PARAGRAPHS: @@ -2624,11 +2657,15 @@ gtk_entry_delete_from_cursor (GtkEntry *entry, GtkEditable *editable = GTK_EDITABLE (entry); gint start_pos = entry->current_pos; gint end_pos = entry->current_pos; + gint old_n_bytes = entry->n_bytes; _gtk_entry_reset_im_context (entry); if (!entry->editable) - return; + { + gtk_widget_error_bell (GTK_WIDGET (entry)); + return; + } if (entry->selection_bound != entry->current_pos) { @@ -2685,7 +2722,10 @@ gtk_entry_delete_from_cursor (GtkEntry *entry, gtk_entry_delete_whitespace (entry); break; } - + + if (entry->n_bytes == old_n_bytes) + gtk_widget_error_bell (GTK_WIDGET (entry)); + gtk_entry_pend_cursor_blink (entry); } @@ -2698,7 +2738,10 @@ gtk_entry_backspace (GtkEntry *entry) _gtk_entry_reset_im_context (entry); if (!entry->editable || !entry->text) - return; + { + gtk_widget_error_bell (GTK_WIDGET (entry)); + return; + } if (entry->selection_bound != entry->current_pos) { @@ -2751,6 +2794,10 @@ gtk_entry_backspace (GtkEntry *entry) g_free (log_attrs); } + else + { + gtk_widget_error_bell (GTK_WIDGET (entry)); + } gtk_entry_pend_cursor_blink (entry); } @@ -2784,6 +2831,10 @@ gtk_entry_cut_clipboard (GtkEntry *entry) if (gtk_editable_get_selection_bounds (editable, &start, &end)) gtk_editable_delete_text (editable, start, end); } + else + { + gtk_widget_error_bell (GTK_WIDGET (entry)); + } } static void @@ -2791,6 +2842,8 @@ gtk_entry_paste_clipboard (GtkEntry *entry) { if (entry->editable) gtk_entry_paste (entry, GDK_NONE); + else + gtk_widget_error_bell (GTK_WIDGET (entry)); } static void diff --git a/gtk/gtkiconview.c b/gtk/gtkiconview.c index 9e066cf938..1ffcddb236 100644 --- a/gtk/gtkiconview.c +++ b/gtk/gtkiconview.c @@ -3796,7 +3796,10 @@ gtk_icon_view_move_cursor_up_down (GtkIconView *icon_view, } if (!item) - return; + { + gtk_widget_error_bell (GTK_WIDGET (icon_view)); + return; + } if (icon_view->priv->ctrl_pressed || !icon_view->priv->shift_pressed || @@ -3847,6 +3850,9 @@ gtk_icon_view_move_cursor_page_up_down (GtkIconView *icon_view, icon_view->priv->cursor_item, count); + if (item == icon_view->priv->cursor_item) + gtk_widget_error_bell (GTK_WIDGET (icon_view)); + if (!item) return; @@ -3915,7 +3921,10 @@ gtk_icon_view_move_cursor_left_right (GtkIconView *icon_view, } if (!item) - return; + { + gtk_widget_error_bell (GTK_WIDGET (icon_view)); + return; + } if (icon_view->priv->ctrl_pressed || !icon_view->priv->shift_pressed || @@ -3958,6 +3967,9 @@ gtk_icon_view_move_cursor_start_end (GtkIconView *icon_view, item = list ? list->data : NULL; + if (item == icon_view->priv->cursor_item) + gtk_widget_error_bell (GTK_WIDGET (icon_view)); + if (!item) return; diff --git a/gtk/gtkimcontextsimple.c b/gtk/gtkimcontextsimple.c index e6267e514e..ea4182bd0a 100644 --- a/gtk/gtkimcontextsimple.c +++ b/gtk/gtkimcontextsimple.c @@ -23,6 +23,8 @@ #include #include "gtkaccelgroup.h" #include "gtkimcontextsimple.h" +#include "gtksettings.h" +#include "gtkwidget.h" #include "gtkintl.h" #include "gtkalias.h" @@ -1175,6 +1177,31 @@ check_hex (GtkIMContextSimple *context_simple, return TRUE; } +static void +beep_window (GdkWindow *window) +{ + GtkWidget *widget; + + gdk_window_get_user_data (window, &widget); + + if (GTK_IS_WIDGET (widget)) + { + gtk_widget_error_bell (widget); + } + else + { + GdkScreen *screen = gdk_drawable_get_screen (GDK_DRAWABLE (window)); + gboolean beep; + + g_object_get (gtk_settings_get_for_screen (screen), + "gtk-error-bell", &beep, + NULL); + + if (beep) + gdk_window_beep (window); + } +} + static gboolean no_sequence_matches (GtkIMContextSimple *context_simple, gint n_compose, @@ -1212,7 +1239,7 @@ no_sequence_matches (GtkIMContextSimple *context_simple, context_simple->compose_buffer[0] = 0; if (n_compose > 1) /* Invalid sequence */ { - gdk_display_beep (gdk_drawable_get_display (event->window)); + beep_window (event->window); return TRUE; } @@ -1317,7 +1344,7 @@ gtk_im_context_simple_filter_keypress (GtkIMContext *context, else { /* invalid hex sequence */ - gdk_display_beep (gdk_drawable_get_display (event->window)); + beep_window (event->window); context_simple->tentative_match = 0; context_simple->in_hex_sequence = FALSE; @@ -1403,7 +1430,7 @@ gtk_im_context_simple_filter_keypress (GtkIMContext *context, { /* invalid hex sequence */ if (n_compose > 0) - gdk_display_beep (gdk_drawable_get_display (event->window)); + beep_window (event->window); context_simple->tentative_match = 0; context_simple->in_hex_sequence = FALSE; @@ -1438,7 +1465,7 @@ gtk_im_context_simple_filter_keypress (GtkIMContext *context, else if (!is_hex_end) { /* non-hex character in hex sequence */ - gdk_display_beep (gdk_drawable_get_display (event->window)); + beep_window (event->window); return TRUE; } @@ -1465,7 +1492,7 @@ gtk_im_context_simple_filter_keypress (GtkIMContext *context, else { /* invalid hex sequence */ - gdk_display_beep (gdk_drawable_get_display (event->window)); + beep_window (event->window); context_simple->tentative_match = 0; context_simple->in_hex_sequence = FALSE; @@ -1473,7 +1500,7 @@ gtk_im_context_simple_filter_keypress (GtkIMContext *context, } } else if (!check_hex (context_simple, n_compose)) - gdk_display_beep (gdk_drawable_get_display (event->window)); + beep_window (event->window); g_signal_emit_by_name (context_simple, "preedit_changed"); diff --git a/gtk/gtklabel.c b/gtk/gtklabel.c index 409bc18e53..cfe21cef71 100644 --- a/gtk/gtklabel.c +++ b/gtk/gtklabel.c @@ -891,8 +891,8 @@ gtk_label_mnemonic_activate (GtkWidget *widget, /* barf if there was nothing to activate */ g_warning ("Couldn't find a target for a mnemonic activation."); - gdk_display_beep (gtk_widget_get_display (widget)); - + gtk_widget_error_bell (widget); + return FALSE; } @@ -3871,12 +3871,13 @@ gtk_label_move_cursor (GtkLabel *label, gint count, gboolean extend_selection) { + gint old_pos; gint new_pos; if (label->select_info == NULL) return; - - new_pos = label->select_info->selection_end; + + old_pos = new_pos = label->select_info->selection_end; if (label->select_info->selection_end != label->select_info->selection_anchor && !extend_selection) @@ -3901,7 +3902,6 @@ gtk_label_move_cursor (GtkLabel *label, new_pos = end_is_left ? label->select_info->selection_end : label->select_info->selection_anchor; else new_pos = !end_is_left ? label->select_info->selection_end : label->select_info->selection_anchor; - break; } case GTK_MOVEMENT_LOGICAL_POSITIONS: @@ -3933,6 +3933,27 @@ gtk_label_move_cursor (GtkLabel *label, break; case GTK_MOVEMENT_VISUAL_POSITIONS: new_pos = gtk_label_move_visually (label, new_pos, count); + if (new_pos == old_pos) + { + if (!extend_selection) + { + if (!gtk_widget_keynav_failed (GTK_WIDGET (label), + count > 0 ? + GTK_DIR_RIGHT : GTK_DIR_LEFT)) + { + GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (label)); + + if (toplevel) + gtk_widget_child_focus (toplevel, + count > 0 ? + GTK_DIR_RIGHT : GTK_DIR_LEFT); + } + } + else + { + gtk_widget_error_bell (GTK_WIDGET (label)); + } + } break; case GTK_MOVEMENT_WORDS: while (count > 0) @@ -3945,12 +3966,16 @@ gtk_label_move_cursor (GtkLabel *label, new_pos = gtk_label_move_backward_word (label, new_pos); count++; } + if (new_pos == old_pos) + gtk_widget_error_bell (GTK_WIDGET (label)); break; case GTK_MOVEMENT_DISPLAY_LINE_ENDS: case GTK_MOVEMENT_PARAGRAPH_ENDS: case GTK_MOVEMENT_BUFFER_ENDS: /* FIXME: Can do better here */ new_pos = count < 0 ? 0 : strlen (label->text); + if (new_pos == old_pos) + gtk_widget_error_bell (GTK_WIDGET (label)); break; case GTK_MOVEMENT_DISPLAY_LINES: case GTK_MOVEMENT_PARAGRAPHS: diff --git a/gtk/gtkmenu.c b/gtk/gtkmenu.c index b44e756e99..be89918fce 100644 --- a/gtk/gtkmenu.c +++ b/gtk/gtkmenu.c @@ -2756,7 +2756,7 @@ gtk_menu_key_press (GtkWidget *widget, * (basically, those items are accelerator-locked). */ /* g_print("item has no path or is locked, menu prefix: %s\n", menu->accel_path); */ - gdk_display_beep (display); + gtk_widget_error_bell (widget); } else { @@ -2785,7 +2785,7 @@ gtk_menu_key_press (GtkWidget *widget, * locked already */ /* g_print("failed to change\n"); */ - gdk_display_beep (display); + gtk_widget_error_bell (widget); } } } diff --git a/gtk/gtkmenushell.c b/gtk/gtkmenushell.c index cb43ce1927..153620af39 100644 --- a/gtk/gtkmenushell.c +++ b/gtk/gtkmenushell.c @@ -1044,17 +1044,27 @@ gtk_menu_shell_move_selected (GtkMenuShell *menu_shell, GList *node = g_list_find (menu_shell->children, menu_shell->active_menu_item); GList *start_node = node; - + gboolean wrap_around; + + g_object_get (gtk_widget_get_settings (GTK_WIDGET (menu_shell)), + "gtk-keynav-wrap-around", &wrap_around, + NULL); + if (distance > 0) { node = node->next; while (node != start_node && (!node || !_gtk_menu_item_is_selectable (node->data))) { - if (!node) - node = menu_shell->children; - else + if (node) node = node->next; + else if (wrap_around) + node = menu_shell->children; + else + { + gtk_widget_error_bell (GTK_WIDGET (menu_shell)); + break; + } } } else @@ -1063,10 +1073,15 @@ gtk_menu_shell_move_selected (GtkMenuShell *menu_shell, while (node != start_node && (!node || !_gtk_menu_item_is_selectable (node->data))) { - if (!node) - node = g_list_last (menu_shell->children); - else + if (node) node = node->prev; + else if (wrap_around) + node = g_list_last (menu_shell->children); + else + { + gtk_widget_error_bell (GTK_WIDGET (menu_shell)); + break; + } } } diff --git a/gtk/gtknotebook.c b/gtk/gtknotebook.c index cd52f6fb28..19a584c618 100644 --- a/gtk/gtknotebook.c +++ b/gtk/gtknotebook.c @@ -1066,14 +1066,33 @@ gtk_notebook_change_current_page (GtkNotebook *notebook, while (offset != 0) { - current = gtk_notebook_search_page (notebook, current, offset < 0 ? STEP_PREV : STEP_NEXT, TRUE); + current = gtk_notebook_search_page (notebook, current, + offset < 0 ? STEP_PREV : STEP_NEXT, + TRUE); + + if (!current) + { + gboolean wrap_around; + + g_object_get (gtk_widget_get_settings (GTK_WIDGET (notebook)), + "gtk-keynav-wrap-around", &wrap_around, + NULL); + + if (wrap_around) + current = gtk_notebook_search_page (notebook, NULL, + offset < 0 ? STEP_PREV : STEP_NEXT, + TRUE); + else + break; + } + offset += offset < 0 ? 1 : -1; } if (current) gtk_notebook_switch_page (notebook, current->data, -1); else - gdk_display_beep (gtk_widget_get_display (GTK_WIDGET (notebook))); + gtk_widget_error_bell (GTK_WIDGET (notebook)); } static GtkDirectionType @@ -3589,11 +3608,24 @@ focus_tabs_move (GtkNotebook *notebook, new_page = gtk_notebook_search_page (notebook, notebook->focus_tab, search_direction, TRUE); + if (!new_page) + { + gboolean wrap_around; + + g_object_get (gtk_widget_get_settings (GTK_WIDGET (notebook)), + "gtk-keynav-wrap-around", &wrap_around, + NULL); + + if (wrap_around) + new_page = gtk_notebook_search_page (notebook, NULL, + search_direction, TRUE); + } + if (new_page) gtk_notebook_switch_focus_tab (notebook, new_page); else - gdk_display_beep (gtk_widget_get_display (GTK_WIDGET (notebook))); - + gtk_widget_error_bell (GTK_WIDGET (notebook)); + return TRUE; } diff --git a/gtk/gtkradiobutton.c b/gtk/gtkradiobutton.c index d1d7f4fe93..91bedc3f81 100644 --- a/gtk/gtkradiobutton.c +++ b/gtk/gtkradiobutton.c @@ -491,6 +491,12 @@ gtk_radio_button_focus (GtkWidget *widget, if (!new_focus) { + if (!gtk_widget_keynav_failed (widget, direction)) + { + g_slist_free (focus_list); + return FALSE; + } + tmp_list = focus_list; while (tmp_list) @@ -511,8 +517,17 @@ gtk_radio_button_focus (GtkWidget *widget, if (new_focus) { + GtkSettings *settings = gtk_widget_get_settings (widget); + gboolean cursor_only; + + g_object_get (settings, + "gtk-keynav-cursor-only", &cursor_only, + NULL); + gtk_widget_grab_focus (new_focus); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (new_focus), TRUE); + + if (!cursor_only) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (new_focus), TRUE); } return TRUE; diff --git a/gtk/gtkrange.c b/gtk/gtkrange.c index 216f9049c8..c6c6e56dda 100644 --- a/gtk/gtkrange.c +++ b/gtk/gtkrange.c @@ -2457,6 +2457,42 @@ static void gtk_range_move_slider (GtkRange *range, GtkScrollType scroll) { + gboolean cursor_only; + + g_object_get (gtk_widget_get_settings (GTK_WIDGET (range)), + "gtk-keynav-cursor-only", &cursor_only, + NULL); + + if (cursor_only) + { + GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (range)); + + if (range->orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (scroll == GTK_SCROLL_STEP_UP || + scroll == GTK_SCROLL_STEP_DOWN) + { + if (toplevel) + gtk_widget_child_focus (toplevel, + scroll == GTK_SCROLL_STEP_UP ? + GTK_DIR_UP : GTK_DIR_DOWN); + return; + } + } + else + { + if (scroll == GTK_SCROLL_STEP_LEFT || + scroll == GTK_SCROLL_STEP_RIGHT) + { + if (toplevel) + gtk_widget_child_focus (toplevel, + scroll == GTK_SCROLL_STEP_LEFT ? + GTK_DIR_LEFT : GTK_DIR_RIGHT); + return; + } + } + } + gtk_range_scroll (range, scroll); /* Policy DELAYED makes sense with key events, diff --git a/gtk/gtksettings.c b/gtk/gtksettings.c index 0054a2fcb4..cd3eda5900 100644 --- a/gtk/gtksettings.c +++ b/gtk/gtksettings.c @@ -93,6 +93,9 @@ enum { PROP_COLOR_SCHEME, PROP_ENABLE_ANIMATIONS, PROP_TOUCHSCREEN_MODE, + PROP_KEYNAV_CURSOR_ONLY, + PROP_KEYNAV_WRAP_AROUND, + PROP_ERROR_BELL, PROP_COLOR_HASH }; @@ -507,7 +510,7 @@ gtk_settings_class_init (GtkSettingsClass *class) /** * GtkSettings:gtk-touchscreen-mode: * - * When TRUE, there are no motion notify events delivered on this screen, + * When %TRUE, there are no motion notify events delivered on this screen, * and widgets can't use the pointer hovering them for any essential * functionality. * @@ -523,6 +526,64 @@ gtk_settings_class_init (GtkSettingsClass *class) g_assert (result == PROP_TOUCHSCREEN_MODE); + /** + * GtkSettings:gtk-keynav-cursor-only: + * + * When %TRUE, keyboard navigation should be able to reach all widgets + * by using the cursor keys only. Tab, Shift etc. keys can't be expected + * to be present on the used input device. + * + * Since: 2.12 + */ + result = settings_install_property_parser (class, + g_param_spec_boolean ("gtk-keynav-cursor-only", + P_("Keynav Cursor Only"), + P_("When TRUE, there are only cursor keys available to navigate widgets"), + FALSE, + GTK_PARAM_READWRITE), + NULL); + + g_assert (result == PROP_KEYNAV_CURSOR_ONLY); + + /** + * GtkSettings:gtk-keynav-wrap-around: + * + * When %TRUE, some widgets will wrap around when doing keyboard + * navigation, such as menus, menubars and notebooks. + * + * Since: 2.12 + */ + result = settings_install_property_parser (class, + g_param_spec_boolean ("gtk-keynav-wrap-around", + P_("Keynav Wrap Around"), + P_("Whether to wrap around when keyboard-navigating widgets"), + TRUE, + GTK_PARAM_READWRITE), + NULL); + + g_assert (result == PROP_KEYNAV_WRAP_AROUND); + + /** + * GtkSettings:gtk-error-bell: + * + * When %TRUE, keyboard navigation and other input-related errors + * will cause a beep. Since the error bell is implemented using + * gdk_window_beep(), the windowing system may offer ways to + * configure the error bell in many ways, such as flashing the + * window or similar visual effects. + * + * Since: 2.12 + */ + result = settings_install_property_parser (class, + g_param_spec_boolean ("gtk-error-bell", + P_("Error Bell"), + P_("When TRUE, keyboard navigation and other errors will cause a beep"), + TRUE, + GTK_PARAM_READWRITE), + NULL); + + g_assert (result == PROP_ERROR_BELL); + /** * GtkSettings:color-hash: * diff --git a/gtk/gtkspinbutton.c b/gtk/gtkspinbutton.c index 7c76829cb6..bcd360bbc4 100644 --- a/gtk/gtkspinbutton.c +++ b/gtk/gtkspinbutton.c @@ -1243,6 +1243,8 @@ static void gtk_spin_button_real_change_value (GtkSpinButton *spin, GtkScrollType scroll) { + gdouble old_value = spin->adjustment->value; + /* We don't test whether the entry is editable, since * this key binding conceptually corresponds to changing * the value with the buttons using the mouse, which @@ -1320,6 +1322,9 @@ gtk_spin_button_real_change_value (GtkSpinButton *spin, } gtk_spin_button_update (spin); + + if (spin->adjustment->value == old_value) + gtk_widget_error_bell (GTK_WIDGET (spin)); } static gint diff --git a/gtk/gtktextview.c b/gtk/gtktextview.c index 69eb696222..d3e5b23711 100644 --- a/gtk/gtktextview.c +++ b/gtk/gtktextview.c @@ -260,10 +260,10 @@ static void gtk_text_view_move_viewport (GtkTextView *text_view, GtkScrollStep step, gint count); static void gtk_text_view_set_anchor (GtkTextView *text_view); -static void gtk_text_view_scroll_pages (GtkTextView *text_view, +static gboolean gtk_text_view_scroll_pages (GtkTextView *text_view, gint count, gboolean extend_selection); -static void gtk_text_view_scroll_hpages (GtkTextView *text_view, +static gboolean gtk_text_view_scroll_hpages(GtkTextView *text_view, gint count, gboolean extend_selection); static void gtk_text_view_insert_at_cursor (GtkTextView *text_view, @@ -3922,6 +3922,9 @@ gtk_text_view_key_press_event (GtkWidget *widget, GdkEventKey *event) gtk_text_view_reset_blink_time (text_view); gtk_text_view_pend_cursor_blink (text_view); + if (!retval && event->length) + gtk_widget_error_bell (widget); + return retval; } @@ -4688,8 +4691,8 @@ gtk_text_view_move_cursor_internal (GtkTextView *text_view, { GtkTextIter insert; GtkTextIter newplace; - gint cursor_x_pos = 0; + GtkDirectionType leave_direction = -1; if (!text_view->cursor_visible) { @@ -4733,14 +4736,18 @@ gtk_text_view_move_cursor_internal (GtkTextView *text_view, if (step == GTK_MOVEMENT_PAGES) { - gtk_text_view_scroll_pages (text_view, count, extend_selection); + if (!gtk_text_view_scroll_pages (text_view, count, extend_selection)) + gtk_widget_error_bell (GTK_WIDGET (text_view)); + gtk_text_view_check_cursor_blink (text_view); gtk_text_view_pend_cursor_blink (text_view); return; } else if (step == GTK_MOVEMENT_HORIZONTAL_PAGES) { - gtk_text_view_scroll_hpages (text_view, count, extend_selection); + if (!gtk_text_view_scroll_hpages (text_view, count, extend_selection)) + gtk_widget_error_bell (GTK_WIDGET (text_view)); + gtk_text_view_check_cursor_blink (text_view); gtk_text_view_pend_cursor_blink (text_view); return; @@ -4777,19 +4784,23 @@ gtk_text_view_move_cursor_internal (GtkTextView *text_view, case GTK_MOVEMENT_DISPLAY_LINES: if (count < 0) - { - if (gtk_text_view_move_iter_by_lines (text_view, &newplace, count)) - gtk_text_layout_move_iter_to_x (text_view->layout, &newplace, cursor_x_pos); - else - gtk_text_iter_set_line_offset (&newplace, 0); - } + { + leave_direction = GTK_DIR_UP; + + if (gtk_text_view_move_iter_by_lines (text_view, &newplace, count)) + gtk_text_layout_move_iter_to_x (text_view->layout, &newplace, cursor_x_pos); + else + gtk_text_iter_set_line_offset (&newplace, 0); + } if (count > 0) - { - if (gtk_text_view_move_iter_by_lines (text_view, &newplace, count)) - gtk_text_layout_move_iter_to_x (text_view->layout, &newplace, cursor_x_pos); - else - gtk_text_iter_forward_to_line_end (&newplace); - } + { + leave_direction = GTK_DIR_DOWN; + + if (gtk_text_view_move_iter_by_lines (text_view, &newplace, count)) + gtk_text_layout_move_iter_to_x (text_view->layout, &newplace, cursor_x_pos); + else + gtk_text_iter_forward_to_line_end (&newplace); + } break; case GTK_MOVEMENT_DISPLAY_LINE_ENDS: @@ -4860,6 +4871,18 @@ gtk_text_view_move_cursor_internal (GtkTextView *text_view, if (step == GTK_MOVEMENT_DISPLAY_LINES) gtk_text_view_set_virtual_cursor_pos (text_view, cursor_x_pos, -1); } + else if (leave_direction != -1) + { + if (!gtk_widget_keynav_failed (GTK_WIDGET (text_view), + leave_direction)) + { + gtk_text_view_move_focus (text_view, leave_direction); + } + } + else + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } gtk_text_view_check_cursor_blink (text_view); gtk_text_view_pend_cursor_blink (text_view); @@ -4943,7 +4966,7 @@ gtk_text_view_set_anchor (GtkTextView *text_view) gtk_text_buffer_create_mark (get_buffer (text_view), "anchor", &insert, TRUE); } -static void +static gboolean gtk_text_view_scroll_pages (GtkTextView *text_view, gint count, gboolean extend_selection) @@ -4952,11 +4975,12 @@ gtk_text_view_scroll_pages (GtkTextView *text_view, gdouble oldval; GtkAdjustment *adj; gint cursor_x_pos, cursor_y_pos; + GtkTextIter old_insert; GtkTextIter new_insert; GtkTextIter anchor; gint y0, y1; - g_return_if_fail (text_view->vadjustment != NULL); + g_return_val_if_fail (text_view->vadjustment != NULL, FALSE); adj = text_view->vadjustment; @@ -4970,9 +4994,13 @@ gtk_text_view_scroll_pages (GtkTextView *text_view, gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); - -/* Validate the region that will be brought into view by the cursor motion + + /* Validate the region that will be brought into view by the cursor motion */ + gtk_text_buffer_get_iter_at_mark (get_buffer (text_view), + &old_insert, + gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); + if (count < 0) { gtk_text_view_get_first_para_iter (text_view, &anchor); @@ -4989,6 +5017,8 @@ gtk_text_view_scroll_pages (GtkTextView *text_view, gtk_text_layout_validate_yrange (text_view->layout, &anchor, y0, y1); /* FIXME do we need to update the adjustment ranges here? */ + new_insert = old_insert; + if (count < 0 && adj->value <= (adj->lower + 1e-12)) { /* already at top, just be sure we are at offset 0 */ @@ -5005,9 +5035,9 @@ gtk_text_view_scroll_pages (GtkTextView *text_view, { gtk_text_view_get_virtual_cursor_pos (text_view, &cursor_x_pos, &cursor_y_pos); - newval = adj->value; oldval = adj->value; - + newval = adj->value; + newval += count * adj->page_increment; set_adjustment_clamped (adj, newval); @@ -5027,9 +5057,11 @@ gtk_text_view_scroll_pages (GtkTextView *text_view, gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); + + return !gtk_text_iter_equal (&old_insert, &new_insert); } -static void +static gboolean gtk_text_view_scroll_hpages (GtkTextView *text_view, gint count, gboolean extend_selection) @@ -5038,10 +5070,11 @@ gtk_text_view_scroll_hpages (GtkTextView *text_view, gdouble oldval; GtkAdjustment *adj; gint cursor_x_pos, cursor_y_pos; + GtkTextIter old_insert; GtkTextIter new_insert; gint y, height; - g_return_if_fail (text_view->hadjustment != NULL); + g_return_val_if_fail (text_view->hadjustment != NULL, FALSE); adj = text_view->hadjustment; @@ -5055,16 +5088,18 @@ gtk_text_view_scroll_hpages (GtkTextView *text_view, gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); - + /* Validate the line that we're moving within. */ gtk_text_buffer_get_iter_at_mark (get_buffer (text_view), - &new_insert, + &old_insert, gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); gtk_text_layout_get_line_yrange (text_view->layout, &new_insert, &y, &height); gtk_text_layout_validate_yrange (text_view->layout, &new_insert, y, y + height); /* FIXME do we need to update the adjustment ranges here? */ - + + new_insert = old_insert; + if (count < 0 && adj->value <= (adj->lower + 1e-12)) { /* already at far left, just be sure we are at offset 0 */ @@ -5082,9 +5117,9 @@ gtk_text_view_scroll_hpages (GtkTextView *text_view, { gtk_text_view_get_virtual_cursor_pos (text_view, &cursor_x_pos, &cursor_y_pos); - newval = adj->value; oldval = adj->value; - + newval = adj->value; + newval += count * adj->page_increment; set_adjustment_clamped (adj, newval); @@ -5110,6 +5145,8 @@ gtk_text_view_scroll_hpages (GtkTextView *text_view, gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); + + return !gtk_text_iter_equal (&old_insert, &new_insert); } static gboolean @@ -5143,8 +5180,11 @@ static void gtk_text_view_insert_at_cursor (GtkTextView *text_view, const gchar *str) { - gtk_text_buffer_insert_interactive_at_cursor (get_buffer (text_view), str, -1, - text_view->editable); + if (!gtk_text_buffer_insert_interactive_at_cursor (get_buffer (text_view), str, -1, + text_view->editable)) + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } } static void @@ -5261,6 +5301,10 @@ gtk_text_view_delete_from_cursor (GtkTextView *text_view, " ", 1, text_view->editable); } + else + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } gtk_text_buffer_end_user_action (get_buffer (text_view)); gtk_text_view_set_virtual_cursor_pos (text_view, -1, -1); @@ -5269,6 +5313,10 @@ gtk_text_view_delete_from_cursor (GtkTextView *text_view, gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_mark (get_buffer (text_view), "insert")); } + else + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } } static void @@ -5295,6 +5343,10 @@ gtk_text_view_backspace (GtkTextView *text_view) gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_insert (get_buffer (text_view))); } + else + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } } static void @@ -6337,9 +6389,13 @@ insert_text_data (GtkTextView *text_view, if (str) { - gtk_text_buffer_insert_interactive (get_buffer (text_view), - drop_point, (gchar *) str, -1, - text_view->editable); + if (!gtk_text_buffer_insert_interactive (get_buffer (text_view), + drop_point, (gchar *) str, -1, + text_view->editable)) + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } + g_free (str); } } @@ -6804,8 +6860,11 @@ gtk_text_view_commit_text (GtkTextView *text_view, if (!strcmp (str, "\n")) { - gtk_text_buffer_insert_interactive_at_cursor (get_buffer (text_view), "\n", 1, - text_view->editable); + if (!gtk_text_buffer_insert_interactive_at_cursor (get_buffer (text_view), "\n", 1, + text_view->editable)) + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } } else { @@ -6820,8 +6879,12 @@ gtk_text_view_commit_text (GtkTextView *text_view, if (!gtk_text_iter_ends_line (&insert)) gtk_text_view_delete_from_cursor (text_view, GTK_DELETE_CHARS, 1); } - gtk_text_buffer_insert_interactive_at_cursor (get_buffer (text_view), str, -1, - text_view->editable); + + if (!gtk_text_buffer_insert_interactive_at_cursor (get_buffer (text_view), str, -1, + text_view->editable)) + { + gtk_widget_error_bell (GTK_WIDGET (text_view)); + } } gtk_text_buffer_end_user_action (get_buffer (text_view)); diff --git a/gtk/gtktreeview.c b/gtk/gtktreeview.c index 2f6a56e405..e8190d1669 100644 --- a/gtk/gtktreeview.c +++ b/gtk/gtktreeview.c @@ -413,7 +413,7 @@ static gboolean gtk_tree_view_search_scroll_event (GtkWidget *entry static gboolean gtk_tree_view_search_key_press_event (GtkWidget *entry, GdkEventKey *event, GtkTreeView *tree_view); -static void gtk_tree_view_search_move (GtkWidget *window, +static gboolean gtk_tree_view_search_move (GtkWidget *window, GtkTreeView *tree_view, gboolean up); static gboolean gtk_tree_view_search_equal_func (GtkTreeModel *model, @@ -5115,11 +5115,16 @@ gtk_tree_view_key_press (GtkWidget *widget, GtkTreeViewColumn *column = GTK_TREE_VIEW_COLUMN (focus_column->data); if (!column->resizable) - return TRUE; + { + gtk_widget_error_bell (widget); + return TRUE; + } if (event->keyval == (rtl ? GDK_Right : GDK_Left) || event->keyval == (rtl ? GDK_KP_Right : GDK_KP_Left)) { + gint old_width = column->resized_width; + column->resized_width = MAX (column->resized_width, column->width); column->resized_width -= 2; @@ -5138,11 +5143,17 @@ gtk_tree_view_key_press (GtkWidget *widget, column->max_width); column->use_resized_width = TRUE; - gtk_widget_queue_resize (widget); + + if (column->resized_width != old_width) + gtk_widget_queue_resize (widget); + else + gtk_widget_error_bell (widget); } else if (event->keyval == (rtl ? GDK_Left : GDK_Right) || event->keyval == (rtl ? GDK_KP_Left : GDK_KP_Right)) { + gint old_width = column->resized_width; + column->resized_width = MAX (column->resized_width, column->width); column->resized_width += 2; @@ -5152,7 +5163,11 @@ gtk_tree_view_key_press (GtkWidget *widget, column->max_width); column->use_resized_width = TRUE; - gtk_widget_queue_resize (widget); + + if (column->resized_width != old_width) + gtk_widget_queue_resize (widget); + else + gtk_widget_error_bell (widget); } return TRUE; @@ -5174,6 +5189,8 @@ gtk_tree_view_key_press (GtkWidget *widget, col = gtk_tree_view_get_drop_column (tree_view, column, DROP_LEFT); if (col != (GtkTreeViewColumn *)0x1) gtk_tree_view_move_column_after (tree_view, column, col); + else + gtk_widget_error_bell (widget); } else if (event->keyval == (rtl ? GDK_Left : GDK_Right) || event->keyval == (rtl ? GDK_KP_Left : GDK_KP_Right)) @@ -5182,6 +5199,8 @@ gtk_tree_view_key_press (GtkWidget *widget, col = gtk_tree_view_get_drop_column (tree_view, column, DROP_RIGHT); if (col != (GtkTreeViewColumn *)0x1) gtk_tree_view_move_column_after (tree_view, column, col); + else + gtk_widget_error_bell (widget); } else if (event->keyval == GDK_Home || event->keyval == GDK_KP_Home) { @@ -5189,6 +5208,8 @@ gtk_tree_view_key_press (GtkWidget *widget, col = gtk_tree_view_get_drop_column (tree_view, column, DROP_HOME); if (col != (GtkTreeViewColumn *)0x1) gtk_tree_view_move_column_after (tree_view, column, col); + else + gtk_widget_error_bell (widget); } else if (event->keyval == GDK_End || event->keyval == GDK_KP_End) { @@ -5196,6 +5217,8 @@ gtk_tree_view_key_press (GtkWidget *widget, col = gtk_tree_view_get_drop_column (tree_view, column, DROP_END); if (col != (GtkTreeViewColumn *)0x1) gtk_tree_view_move_column_after (tree_view, column, col); + else + gtk_widget_error_bell (widget); } return TRUE; @@ -5206,8 +5229,7 @@ gtk_tree_view_key_press (GtkWidget *widget, || event->keyval == GDK_Right || event->keyval == GDK_KP_Right)) { if ((event->keyval == (rtl ? GDK_Right : GDK_Left) - || event->keyval == (rtl ? GDK_KP_Right : GDK_KP_Left)) - && focus_column->prev) + || event->keyval == (rtl ? GDK_KP_Right : GDK_KP_Left))) { GList *tmp; @@ -5216,7 +5238,10 @@ gtk_tree_view_key_press (GtkWidget *widget, break; if (!tmp) - return FALSE; + { + gtk_widget_error_bell (widget); + return TRUE; + } tree_view->priv->focus_column = GTK_TREE_VIEW_COLUMN (tmp->data); gtk_widget_grab_focus (tree_view->priv->focus_column->button); @@ -5227,8 +5252,7 @@ gtk_tree_view_key_press (GtkWidget *widget, tree_view->priv->hadjustment->upper - tree_view->priv->hadjustment->page_size)); } else if ((event->keyval == (rtl ? GDK_Left : GDK_Right) - || event->keyval == (rtl ? GDK_KP_Left : GDK_KP_Right)) - && focus_column->next) + || event->keyval == (rtl ? GDK_KP_Left : GDK_KP_Right))) { GList *tmp; @@ -5237,7 +5261,10 @@ gtk_tree_view_key_press (GtkWidget *widget, break; if (!tmp) - return FALSE; + { + gtk_widget_error_bell (widget); + return TRUE; + } tree_view->priv->focus_column = GTK_TREE_VIEW_COLUMN (tmp->data); @@ -9558,6 +9585,7 @@ gtk_tree_view_move_cursor_up_down (GtkTreeView *tree_view, else { gtk_tree_view_clamp_node_visible (tree_view, cursor_tree, cursor_node); + gtk_widget_error_bell (GTK_WIDGET (tree_view)); } gtk_widget_grab_focus (GTK_WIDGET (tree_view)); @@ -9569,6 +9597,7 @@ gtk_tree_view_move_cursor_page_up_down (GtkTreeView *tree_view, { GtkRBTree *cursor_tree = NULL; GtkRBNode *cursor_node = NULL; + GtkTreePath *old_cursor_path = NULL; GtkTreePath *cursor_path = NULL; gint y; gint window_y; @@ -9578,20 +9607,21 @@ gtk_tree_view_move_cursor_page_up_down (GtkTreeView *tree_view, return; if (gtk_tree_row_reference_valid (tree_view->priv->cursor)) - cursor_path = gtk_tree_row_reference_get_path (tree_view->priv->cursor); + old_cursor_path = gtk_tree_row_reference_get_path (tree_view->priv->cursor); else /* This is sorta weird. Focus in should give us a cursor */ return; gtk_widget_style_get (GTK_WIDGET (tree_view), "vertical-separator", &vertical_separator, NULL); - _gtk_tree_view_find_node (tree_view, cursor_path, + _gtk_tree_view_find_node (tree_view, old_cursor_path, &cursor_tree, &cursor_node); - gtk_tree_path_free (cursor_path); - if (cursor_tree == NULL) - /* FIXME: we lost the cursor. Should we try to get one? */ - return; + { + /* FIXME: we lost the cursor. Should we try to get one? */ + gtk_tree_path_free (old_cursor_path); + return; + } g_return_if_fail (cursor_node != NULL); y = _gtk_rbtree_node_find_offset (cursor_tree, cursor_node); @@ -9606,11 +9636,16 @@ gtk_tree_view_move_cursor_page_up_down (GtkTreeView *tree_view, cursor_path = _gtk_tree_view_find_path (tree_view, cursor_tree, cursor_node); g_return_if_fail (cursor_path != NULL); gtk_tree_view_real_set_cursor (tree_view, cursor_path, TRUE, FALSE); - gtk_tree_path_free (cursor_path); y -= window_y; gtk_tree_view_scroll_to_point (tree_view, -1, y); _gtk_tree_view_queue_draw_node (tree_view, cursor_tree, cursor_node, NULL); + + if (!gtk_tree_path_compare (old_cursor_path, cursor_path)) + gtk_widget_error_bell (GTK_WIDGET (tree_view)); + + gtk_tree_path_free (old_cursor_path); + gtk_tree_path_free (cursor_path); } static void @@ -9703,6 +9738,11 @@ gtk_tree_view_move_cursor_left_right (GtkTreeView *tree_view, NULL); g_signal_emit (tree_view, tree_view_signals[CURSOR_CHANGED], 0); } + else + { + gtk_widget_error_bell (GTK_WIDGET (tree_view)); + } + gtk_tree_view_clamp_column_visible (tree_view, tree_view->priv->focus_column); } @@ -9713,23 +9753,25 @@ gtk_tree_view_move_cursor_start_end (GtkTreeView *tree_view, GtkRBTree *cursor_tree; GtkRBNode *cursor_node; GtkTreePath *path; + GtkTreePath *old_path; if (! GTK_WIDGET_HAS_FOCUS (tree_view)) return; g_return_if_fail (tree_view->priv->tree != NULL); + gtk_tree_view_get_cursor (tree_view, &old_path, NULL); + + cursor_tree = tree_view->priv->tree; + cursor_node = cursor_tree->root; + if (count == -1) { - cursor_tree = tree_view->priv->tree; - cursor_node = cursor_tree->root; while (cursor_node && cursor_node->left != cursor_tree->nil) cursor_node = cursor_node->left; } else { - cursor_tree = tree_view->priv->tree; - cursor_node = cursor_tree->root; do { while (cursor_node && cursor_node->right != cursor_tree->nil) @@ -9744,7 +9786,17 @@ gtk_tree_view_move_cursor_start_end (GtkTreeView *tree_view, } path = _gtk_tree_view_find_path (tree_view, cursor_tree, cursor_node); - gtk_tree_view_real_set_cursor (tree_view, path, TRUE, TRUE); + + if (gtk_tree_path_compare (old_path, path)) + { + gtk_tree_view_real_set_cursor (tree_view, path, TRUE, TRUE); + } + else + { + gtk_widget_error_bell (GTK_WIDGET (tree_view)); + } + + gtk_tree_path_free (old_path); gtk_tree_path_free (path); } @@ -13888,28 +13940,36 @@ gtk_tree_view_search_key_press_event (GtkWidget *widget, /* select previous matching iter */ if (event->keyval == GDK_Up || event->keyval == GDK_KP_Up) { - gtk_tree_view_search_move (widget, tree_view, TRUE); + if (!gtk_tree_view_search_move (widget, tree_view, TRUE)) + gtk_widget_error_bell (widget); + retval = TRUE; } if (((event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) == (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) && (event->keyval == GDK_g || event->keyval == GDK_G)) { - gtk_tree_view_search_move (widget, tree_view, TRUE); + if (!gtk_tree_view_search_move (widget, tree_view, TRUE)) + gtk_widget_error_bell (widget); + retval = TRUE; } /* select next matching iter */ if (event->keyval == GDK_Down || event->keyval == GDK_KP_Down) { - gtk_tree_view_search_move (widget, tree_view, FALSE); + if (!gtk_tree_view_search_move (widget, tree_view, FALSE)) + gtk_widget_error_bell (widget); + retval = TRUE; } if (((event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) == GDK_CONTROL_MASK) && (event->keyval == GDK_g || event->keyval == GDK_G)) { - gtk_tree_view_search_move (widget, tree_view, FALSE); + if (!gtk_tree_view_search_move (widget, tree_view, FALSE)) + gtk_widget_error_bell (widget); + retval = TRUE; } @@ -13927,7 +13987,10 @@ gtk_tree_view_search_key_press_event (GtkWidget *widget, return retval; } -static void +/* this function returns FALSE if there is a search string but + * nothing was found, and TRUE otherwise. + */ +static gboolean gtk_tree_view_search_move (GtkWidget *window, GtkTreeView *tree_view, gboolean up) @@ -13942,15 +14005,17 @@ gtk_tree_view_search_move (GtkWidget *window, text = gtk_entry_get_text (GTK_ENTRY (tree_view->priv->search_entry)); - g_return_if_fail (text != NULL); + g_return_val_if_fail (text != NULL, FALSE); + + len = strlen (text); if (up && tree_view->priv->selected_iter == 1) - return; + return strlen (text) < 1; len = strlen (text); if (len < 1) - return; + return TRUE; model = gtk_tree_view_get_model (tree_view); selection = gtk_tree_view_get_selection (tree_view); @@ -13958,7 +14023,7 @@ gtk_tree_view_search_move (GtkWidget *window, /* search */ gtk_tree_selection_unselect_all (selection); if (!gtk_tree_model_get_iter_first (model, &iter)) - return; + return TRUE; ret = gtk_tree_view_search_iter (model, selection, &iter, text, &count, up?((tree_view->priv->selected_iter) - 1):((tree_view->priv->selected_iter + 1))); @@ -13967,6 +14032,7 @@ gtk_tree_view_search_move (GtkWidget *window, { /* found */ tree_view->priv->selected_iter += up?(-1):(1); + return TRUE; } else { @@ -13976,6 +14042,7 @@ gtk_tree_view_search_move (GtkWidget *window, gtk_tree_view_search_iter (model, selection, &iter, text, &count, tree_view->priv->selected_iter); + return FALSE; } } diff --git a/gtk/gtkwidget.c b/gtk/gtkwidget.c index 392ebf5185..ae16cb8fd6 100644 --- a/gtk/gtkwidget.c +++ b/gtk/gtkwidget.c @@ -121,6 +121,7 @@ enum { CAN_ACTIVATE_ACCEL, GRAB_BROKEN, COMPOSITED_CHANGED, + KEYNAV_FAILED, LAST_SIGNAL }; @@ -204,6 +205,8 @@ static gboolean gtk_widget_real_focus_out_event (GtkWidget *widget, GdkEventFocus *event); static gboolean gtk_widget_real_focus (GtkWidget *widget, GtkDirectionType direction); +static gboolean gtk_widget_real_keynav_failed (GtkWidget *widget, + GtkDirectionType direction); static PangoContext* gtk_widget_peek_pango_context (GtkWidget *widget); static void gtk_widget_update_pango_context (GtkWidget *widget); static void gtk_widget_propagate_state (GtkWidget *widget, @@ -807,6 +810,29 @@ gtk_widget_class_init (GtkWidgetClass *klass) _gtk_marshal_VOID__VOID, G_TYPE_NONE, 0); + /** + * GtkWidget::keynav-failed: + * @widget: the object which received the signal. + * @direction: the direction of movement + * + * See gtk_widget_keynav_failed() for details. + * + * Returns: %TRUE if stopping keyboard navigation is fine, %FALSE + * if the emitting widget should try to handle the keyboard + * navigation attempt in its parent container(s). + * + * Since: 2.12 + **/ + widget_signals[KEYNAV_FAILED] = + _gtk_binding_signal_new (I_("keynav-failed"), + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_CALLBACK (gtk_widget_real_keynav_failed), + _gtk_boolean_handled_accumulator, NULL, + _gtk_marshal_BOOLEAN__ENUM, + G_TYPE_BOOLEAN, 1, + GTK_TYPE_DIRECTION_TYPE); + /** * GtkWidget::delete-event: * @widget: the object which received the signal. @@ -3642,7 +3668,7 @@ gtk_widget_real_mnemonic_activate (GtkWidget *widget, { g_warning ("widget `%s' isn't suitable for mnemonic activation", G_OBJECT_TYPE_NAME (widget)); - gdk_display_beep (gtk_widget_get_display (widget)); + gtk_widget_error_bell (widget); } return TRUE; } @@ -4326,6 +4352,35 @@ gtk_widget_real_focus (GtkWidget *widget, return FALSE; } +static gboolean +gtk_widget_real_keynav_failed (GtkWidget *widget, + GtkDirectionType direction) +{ + gboolean cursor_only; + + switch (direction) + { + case GTK_DIR_TAB_FORWARD: + case GTK_DIR_TAB_BACKWARD: + return FALSE; + + case GTK_DIR_UP: + case GTK_DIR_DOWN: + case GTK_DIR_LEFT: + case GTK_DIR_RIGHT: + g_object_get (gtk_widget_get_settings (widget), + "gtk-keynav-cursor-only", &cursor_only, + NULL); + if (cursor_only) + return FALSE; + break; + } + + gtk_widget_error_bell (widget); + + return TRUE; +} + /** * gtk_widget_is_focus: * @widget: a #GtkWidget @@ -5899,6 +5954,92 @@ gtk_widget_child_focus (GtkWidget *widget, return return_val; } +/** + * gtk_widget_keynav_failed: + * @widget: a #GtkWidget + * @direction: direction of focus movement + * + * This function should be called whenever keyboard navigation within + * a single widget hits a boundary. The function emits the + * "keynav-changed" signal on the widget and its return value should + * be interpreted in a way similar to the return value of + * gtk_widget_child_focus(): + * + * When %TRUE is returned, stay in the widget, the failed keyboard + * navigation is Ok and/or there is nowhere we can/should move the + * focus to. + * + * When %FALSE is returned, the caller should continue with keyboard + * navigation outside the widget, e.g. by calling + * gtk_widget_child_focus() on the widget's toplevel. + * + * The default implementation for the "keynav-failed" signal is to + * return %TRUE for %GTK_DIR_TAB_FORWARD and + * %GTK_DIR_TAB_BACKWARD. For the other values of #GtkDirectionType, + * it looks at the "gtk-keynav-cursor-only" settings property and + * returns %FALSE if the setting is %TRUE. This way the entire GUI + * becomes cursor-navigatable on input devices such as mobile phones + * which only have cursor keys but no tab key. + * + * Whenever the default implementation returns %TRUE, it also calls + * gtk_widget_error_bell() to notify the user of the failed keyboard + * navigation. + * + * A use case for providing an own implementation of keynav-failed (by + * either connecting to it or by overriding it) would be a row of + * #GtkEntry widgets where the user should be able to navigate the + * entire row with the cursor keys, as e.g. known from GUIs that + * require entering license keys. + * + * Return value: %TRUE if stopping keyboard navigation is fine, %FALSE + * if the emitting widget should try to handle the keyboard + * navigation attempt in its parent container(s). + * + * Since: 2.12 + **/ +gboolean +gtk_widget_keynav_failed (GtkWidget *widget, + GtkDirectionType direction) +{ + gboolean return_val; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + + g_signal_emit (widget, widget_signals[KEYNAV_FAILED], 0, + direction, &return_val); + + return return_val; +} + +/** + * gtk_widget_error_bell: + * @widget: a #GtkWidget + * + * Notifies the user about an input-related error on this widget. If + * the gtk-error-bell settings property is %TRUE, it calls + * gdk_window_beep(), otherwise it does nothing. + * + * Note that the effect of gdk_window_beep() can be configured in many + * ways, depending on the windowing backend and the desktop environment + * or window manager that is used. + * + * Since: 2.12 + **/ +void +gtk_widget_error_bell (GtkWidget *widget) +{ + gboolean beep; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + + g_object_get (gtk_widget_get_settings (widget), + "gtk-error-bell", &beep, + NULL); + + if (beep && widget->window) + gdk_window_beep (widget->window); +} + /** * gtk_widget_set_uposition: * @widget: a #GtkWidget diff --git a/gtk/gtkwidget.h b/gtk/gtkwidget.h index 7cfc3df5cd..cb5841fbd3 100644 --- a/gtk/gtkwidget.h +++ b/gtk/gtkwidget.h @@ -408,7 +408,7 @@ struct _GtkWidgetClass GdkEventGrabBroken *event); void (* composited_changed) (GtkWidget *widget); - + /* Padding for future expansion */ void (*_gtk_reserved4) (void); void (*_gtk_reserved5) (void); @@ -563,6 +563,9 @@ GdkWindow *gtk_widget_get_parent_window (GtkWidget *widget); gboolean gtk_widget_child_focus (GtkWidget *widget, GtkDirectionType direction); +gboolean gtk_widget_keynav_failed (GtkWidget *widget, + GtkDirectionType direction); +void gtk_widget_error_bell (GtkWidget *widget); void gtk_widget_set_size_request (GtkWidget *widget, gint width,