From 07261e284ca773da88c46d52c213a68d5fa5e152 Mon Sep 17 00:00:00 2001 From: Christian Dywan Date: Sun, 17 Jan 2010 17:45:34 +0100 Subject: [PATCH] Re-implement location action with our own completion popup Completion is triggered on key press only, with a delay of 150ms. Completion can be triggered by input method usage. The popup size adjusts to the entry size and closes if focus is lost. --- midori/midori-locationaction.c | 398 +++++++++++++++++++++++++-------- 1 file changed, 301 insertions(+), 97 deletions(-) diff --git a/midori/midori-locationaction.c b/midori/midori-locationaction.c index c353b92a..b2314e19 100644 --- a/midori/midori-locationaction.c +++ b/midori/midori-locationaction.c @@ -21,6 +21,7 @@ #include #include +#define COMPLETION_DELAY 150 #define MAX_ITEMS 25 struct _MidoriLocationAction @@ -34,6 +35,12 @@ struct _MidoriLocationAction GtkTreeModel* model; GtkTreeModel* filter_model; + guint completion_timeout; + gchar* key; + GtkWidget* popup; + GtkWidget* treeview; + GtkTreeModel* completion_model; + GtkWidget* entry; GdkPixbuf* default_icon; GHashTable* items; KatzeNet* net; @@ -109,9 +116,17 @@ static void midori_location_action_disconnect_proxy (GtkAction* action, GtkWidget* proxy); +static gboolean +midori_location_entry_completion_match_cb (GtkTreeModel* model, + GtkTreeIter* iter, + gpointer data); + static void -midori_location_action_completion_init (MidoriLocationAction* location_action, - GtkEntry* entry); +midori_location_entry_render_text_cb (GtkCellLayout* layout, + GtkCellRenderer* renderer, + GtkTreeModel* model, + GtkTreeIter* iter, + gpointer data); static void midori_location_action_class_init (MidoriLocationActionClass* class) @@ -247,6 +262,153 @@ midori_location_action_class_init (MidoriLocationActionClass* class) "style \"midori-location-entry-style\"\n"); } +static GtkTreeModel* +midori_location_action_create_model (void) +{ + GtkTreeModel* model = (GtkTreeModel*) gtk_list_store_new (N_COLS, + GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_INT, G_TYPE_BOOLEAN, G_TYPE_FLOAT); + return model; +} + +static void +midori_location_action_popup_position (GtkWidget* popup, + GtkWidget* widget) +{ + gint wx, wy; + GtkRequisition menu_req; + GtkRequisition widget_req; + + if (GTK_WIDGET_NO_WINDOW (widget)) + { + gdk_window_get_position (widget->window, &wx, &wy); + wx += widget->allocation.x; + wy += widget->allocation.y; + } + else + gdk_window_get_origin (widget->window, &wx, &wy); + gtk_widget_size_request (popup, &menu_req); + gtk_widget_size_request (widget, &widget_req); + + gtk_window_move (GTK_WINDOW (popup), + wx, wy + widget_req.height); + gtk_window_resize (GTK_WINDOW (popup), + widget->allocation.width, 1); +} + +static gboolean +midori_location_action_popup_timeout_cb (gpointer data) +{ + MidoriLocationAction* action = data; + static GtkTreeModel* model = NULL; + GtkTreeViewColumn* column; + gint matches, height, screen_height; + + if (G_UNLIKELY (!action->popup)) + { + GtkWidget* popup; + GtkWidget* scrolled; + GtkWidget* treeview; + GtkCellRenderer* renderer; + + model = gtk_tree_model_filter_new (action->model, NULL); + gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (model), + midori_location_entry_completion_match_cb, action, NULL); + action->completion_model = model; + + popup = gtk_window_new (GTK_WINDOW_POPUP); + gtk_window_set_type_hint (GTK_WINDOW (popup), GDK_WINDOW_TYPE_HINT_COMBO); + scrolled = g_object_new (GTK_TYPE_SCROLLED_WINDOW, + "hscrollbar-policy", GTK_POLICY_NEVER, + "vscrollbar-policy", GTK_POLICY_AUTOMATIC, NULL); + gtk_container_add (GTK_CONTAINER (popup), scrolled); + treeview = gtk_tree_view_new_with_model (model); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE); + gtk_container_add (GTK_CONTAINER (scrolled), treeview); + /* FIXME: Handle button presses and hovering rows */ + action->treeview = treeview; + + column = gtk_tree_view_column_new (); + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (column), renderer, + "pixbuf", FAVICON_COL, "yalign", YALIGN_COL, NULL); + renderer = gtk_cell_renderer_text_new (); + g_object_set_data (G_OBJECT (renderer), "location-action", action); + gtk_cell_renderer_set_fixed_size (renderer, 1, -1); + gtk_cell_renderer_text_set_fixed_height_from_font ( + GTK_CELL_RENDERER_TEXT (renderer), 2); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), renderer, TRUE); + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), renderer, + midori_location_entry_render_text_cb, + action->entry, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); + + action->popup = popup; + g_signal_connect (popup, "destroy", + G_CALLBACK (gtk_widget_destroyed), &action->popup); + } + + if (!*action->key) + { + const gchar* uri = gtk_entry_get_text (GTK_ENTRY (action->entry)); + katze_assign (action->key, katze_collfold (uri)); + } + + gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (model)); + matches = gtk_tree_model_iter_n_children (model, NULL); + /* TODO: Suggest _("Search with %s") or opening hostname as actions */ + + if (!GTK_WIDGET_VISIBLE (action->popup)) + { + GtkWidget* toplevel = gtk_widget_get_toplevel (action->entry); + gtk_window_set_screen (GTK_WINDOW (action->popup), + gtk_widget_get_screen (action->entry)); + gtk_window_set_transient_for (GTK_WINDOW (action->popup), GTK_WINDOW (toplevel)); + gtk_widget_show_all (action->popup); + gtk_tree_view_columns_autosize (GTK_TREE_VIEW (action->treeview)); + } + + column = gtk_tree_view_get_column (GTK_TREE_VIEW (action->treeview), 0); + gtk_tree_view_column_cell_get_size (column, NULL, NULL, NULL, NULL, &height); + /* FIXME: This really should consider monitor geometry */ + /* FIXME: Consider y position versus height */ + screen_height = gdk_screen_get_height (gtk_widget_get_screen (action->popup)); + height = MIN (matches * height, screen_height / 1.5); + gtk_widget_set_size_request (action->treeview, -1, height); + midori_location_action_popup_position (action->popup, action->entry); + + return FALSE; +} + +static void +midori_location_action_popup_completion (MidoriLocationAction* action, + GtkWidget* entry, + const gchar* key) +{ + if (action->completion_timeout) + g_source_remove (action->completion_timeout); + katze_assign (action->key, g_strdup (key)); + action->entry = entry; + g_signal_connect (entry, "destroy", + G_CALLBACK (gtk_widget_destroyed), &action->entry); + action->completion_timeout = g_timeout_add (COMPLETION_DELAY, + midori_location_action_popup_timeout_cb, action); + /* TODO: Inline completion */ +} + +static void +midori_location_action_popdown_completion (MidoriLocationAction* location_action) +{ + if (G_LIKELY (location_action->popup)) + { + gtk_widget_hide (location_action->popup); + gtk_tree_selection_unselect_all (gtk_tree_view_get_selection ( + GTK_TREE_VIEW (location_action->treeview))); + } + location_action->completion_timeout = 0; +} + /* Allow this to be used in tests, it's otherwise private */ /*static*/ GtkWidget* midori_location_action_entry_for_proxy (GtkWidget* proxy) @@ -273,7 +435,6 @@ midori_location_action_set_model (MidoriLocationAction* location_action, entry = gtk_bin_get_child (GTK_BIN (location_entry)); g_object_set (location_entry, "model", model, NULL); - midori_location_action_completion_init (location_action, GTK_ENTRY (entry)); } } @@ -347,15 +508,6 @@ midori_location_action_thaw (MidoriLocationAction* location_action) } } -static GtkTreeModel* -midori_location_action_create_model (void) -{ - GtkTreeModel* model = (GtkTreeModel*) gtk_list_store_new (N_COLS, - GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, - G_TYPE_INT, G_TYPE_BOOLEAN, G_TYPE_FLOAT); - return model; -} - static void midori_location_action_init (MidoriLocationAction* location_action) { @@ -363,6 +515,10 @@ midori_location_action_init (MidoriLocationAction* location_action) location_action->progress = 0.0; location_action->secondary_icon = NULL; location_action->default_icon = NULL; + location_action->completion_timeout = 0; + location_action->key = NULL; + location_action->popup = NULL; + location_action->entry = NULL; location_action->model = midori_location_action_create_model (); @@ -385,6 +541,12 @@ midori_location_action_finalize (GObject* object) katze_object_assign (location_action->model, NULL); katze_object_assign (location_action->filter_model, NULL); + katze_assign (location_action->key, NULL); + if (location_action->popup) + { + gtk_widget_destroy (location_action->popup); + location_action->popup = NULL; + } katze_object_assign (location_action->default_icon, NULL); g_hash_table_destroy (location_action->items); @@ -584,43 +746,138 @@ midori_location_action_key_press_event_cb (GtkEntry* entry, GdkEventKey* event, GtkAction* action) { - const gchar* uri; + GtkWidget* widget = GTK_WIDGET (entry); + MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (action); + const gchar* text; + static gint selected = -1; + gboolean is_enter = FALSE; switch (event->keyval) { case GDK_ISO_Enter: case GDK_KP_Enter: case GDK_Return: - { - if ((uri = gtk_entry_get_text (entry)) && *uri) + is_enter = TRUE; + case GDK_Left: + case GDK_KP_Left: + case GDK_Right: + case GDK_KP_Right: + + if (location_action->popup && GTK_WIDGET_VISIBLE (location_action->popup)) { - g_signal_emit (action, signals[SUBMIT_URI], 0, uri, - (event->state & GDK_CONTROL_MASK) ? TRUE : FALSE); - return TRUE; + GtkTreeModel* model = location_action->completion_model; + GtkTreeIter iter; + midori_location_action_popdown_completion (location_action); + if (gtk_tree_model_iter_nth_child (model, &iter, NULL, selected)) + { + gchar* uri; + gtk_tree_model_get (model, &iter, URI_COL, &uri, -1); + gtk_entry_set_text (entry, uri); + + if (is_enter) + g_signal_emit (action, signals[SUBMIT_URI], 0, uri, + (event->state & GDK_CONTROL_MASK) ? TRUE : FALSE); + + g_free (uri); + selected = -1; + return TRUE; + } + selected = -1; } - } + + if (is_enter) + if ((text = gtk_entry_get_text (entry)) && *text) + g_signal_emit (action, signals[SUBMIT_URI], 0, text, + (event->state & GDK_CONTROL_MASK) ? TRUE : FALSE); + break; case GDK_Escape: { + if (location_action->popup && GTK_WIDGET_VISIBLE (location_action->popup)) + { + midori_location_action_popdown_completion (location_action); + text = gtk_entry_get_text (entry); + pango_layout_set_text (gtk_entry_get_layout (entry), text, -1); + selected = -1; + return TRUE; + } + g_signal_emit (action, signals[RESET_URI], 0); - return TRUE; + /* Return FALSE to allow Escape to stop loading */ + return FALSE; } + case GDK_Page_Up: + case GDK_Page_Down: + if (!(location_action->popup && GTK_WIDGET_VISIBLE (location_action->popup))) + return TRUE; case GDK_Down: + case GDK_KP_Down: case GDK_Up: + case GDK_KP_Up: { - GtkWidget* parent = gtk_widget_get_parent (GTK_WIDGET (entry)); + GtkWidget* parent; + + if (location_action->popup && GTK_WIDGET_VISIBLE (location_action->popup)) + { + GtkTreeModel* model = location_action->completion_model; + gint matches = gtk_tree_model_iter_n_children (model, NULL); + GtkTreePath* path; + GtkTreeIter iter; + + if (event->keyval == GDK_Down || event->keyval == GDK_KP_Down) + selected = MIN (selected + 1, matches -1); + else if (event->keyval == GDK_Up || event->keyval == GDK_KP_Up) + selected = MAX (selected - 1, 0); + else if (event->keyval == GDK_Page_Down) + selected = MIN (selected + 14, matches -1); + else if (event->keyval == GDK_Page_Up) + selected = MAX (selected - 14, 0); + + path = gtk_tree_path_new_from_indices (selected, -1); + gtk_tree_view_set_cursor (GTK_TREE_VIEW (location_action->treeview), + path, NULL, FALSE); + gtk_tree_path_free (path); + + if (gtk_tree_model_iter_nth_child (model, &iter, NULL, selected)) + { + gchar* uri; + gtk_tree_model_get (model, &iter, URI_COL, &uri, -1); + /* Update the layout without actually changing the text */ + pango_layout_set_text (gtk_entry_get_layout (entry), uri, -1); + g_free (uri); + } + return TRUE; + } + + parent = gtk_widget_get_parent (widget); if (!katze_object_get_boolean (parent, "popup-shown")) gtk_combo_box_popup (GTK_COMBO_BOX (parent)); return TRUE; } - case GDK_Page_Up: - case GDK_Page_Down: - { - return TRUE; - } + default: + if ((text = gtk_entry_get_text (entry)) && *text) + { + midori_location_action_popup_completion (location_action, widget, ""); + selected = -1; + return FALSE; + } } return FALSE; } +#if GTK_CHECK_VERSION (2, 19, 3) +static void +midori_location_action_preedit_changed_cb (GtkWidget* widget, + const gchar* preedit, + GtkAction* action) +{ + MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (action); + gchar* key = katze_collfold (preedit); + midori_location_action_popup_completion (location_action, + GTK_WIDGET (widget), key); + g_free (key); +} +#endif + static gboolean midori_location_action_focus_in_event_cb (GtkWidget* widget, GdkEventKey* event, @@ -635,6 +892,7 @@ midori_location_action_focus_out_event_cb (GtkWidget* widget, GdkEventKey* event, GtkAction* action) { + midori_location_action_popdown_completion (MIDORI_LOCATION_ACTION (action)); g_signal_emit (action, signals[FOCUS_OUT], 0); return FALSE; } @@ -775,17 +1033,15 @@ midori_location_entry_render_text_cb (GtkCellLayout* layout, } static gboolean -midori_location_entry_completion_match_cb (GtkEntryCompletion* completion, - const gchar* key, - GtkTreeIter* iter, - gpointer data) +midori_location_action_match (GtkTreeModel* model, + const gchar* key, + GtkTreeIter* iter, + gpointer data) { - GtkTreeModel* model; gchar* uri; gchar* title; gboolean match; - model = gtk_entry_completion_get_model (completion); gtk_tree_model_get (model, iter, URI_COL, &uri, TITLE_COL, &title, -1); match = FALSE; @@ -803,6 +1059,15 @@ midori_location_entry_completion_match_cb (GtkEntryCompletion* completion, return match; } +static gboolean +midori_location_entry_completion_match_cb (GtkTreeModel* model, + GtkTreeIter* iter, + gpointer data) +{ + MidoriLocationAction* action = data; + return midori_location_action_match (model, action->key, iter, data); +} + /** * midori_location_action_iter_lookup: * @location_action: a #MidoriLocationAction @@ -960,70 +1225,6 @@ midori_location_action_set_item (MidoriLocationAction* location_action, } } -static gboolean -midori_location_entry_match_selected_cb (GtkEntryCompletion* completion, - GtkTreeModel* model, - GtkTreeIter* iter, - MidoriLocationAction* location_action) -{ - gchar* uri; - gtk_tree_model_get (model, iter, URI_COL, &uri, -1); - - midori_location_action_set_text (location_action, uri); - g_signal_emit (location_action, signals[SUBMIT_URI], 0, uri, FALSE); - g_free (uri); - - return FALSE; -} - -static void -midori_location_action_completion_init (MidoriLocationAction* location_action, - GtkEntry* entry) -{ - GtkEntryCompletion* completion; - GtkCellRenderer* renderer; - - if ((completion = gtk_entry_get_completion (entry))) - { - gtk_entry_completion_set_model (completion, - midori_location_action_is_frozen (location_action) - ? NULL : location_action->model); - return; - } - - completion = gtk_entry_completion_new (); - gtk_entry_set_completion (entry, completion); - g_object_unref (completion); - gtk_entry_completion_set_model (completion, - midori_location_action_is_frozen (location_action) - ? NULL : location_action->model); - - gtk_entry_completion_set_text_column (completion, URI_COL); - #if GTK_CHECK_VERSION (2, 12, 0) - gtk_entry_completion_set_inline_selection (completion, TRUE); - #endif - gtk_cell_layout_clear (GTK_CELL_LAYOUT (completion)); - - renderer = gtk_cell_renderer_pixbuf_new (); - gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (completion), renderer, FALSE); - gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (completion), renderer, - "pixbuf", FAVICON_COL, "yalign", YALIGN_COL, NULL); - renderer = gtk_cell_renderer_text_new (); - g_object_set_data (G_OBJECT (renderer), "location-action", location_action); - gtk_cell_renderer_set_fixed_size (renderer, 1, -1); - gtk_cell_renderer_text_set_fixed_height_from_font ( - GTK_CELL_RENDERER_TEXT (renderer), 2); - gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (completion), renderer, TRUE); - gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (completion), renderer, - midori_location_entry_render_text_cb, - entry, NULL); - gtk_entry_completion_set_match_func (completion, - midori_location_entry_completion_match_cb, NULL, NULL); - - g_signal_connect (completion, "match-selected", - G_CALLBACK (midori_location_entry_match_selected_cb), location_action); -} - static void midori_location_action_entry_changed_cb (GtkComboBox* combo_box, MidoriLocationAction* location_action) @@ -1136,7 +1337,6 @@ midori_location_action_connect_proxy (GtkAction* action, renderer, midori_location_entry_render_text_cb, child, NULL); gtk_combo_box_set_active (GTK_COMBO_BOX (entry), -1); - midori_location_action_completion_init (location_action, GTK_ENTRY (child)); g_signal_connect (entry, "changed", G_CALLBACK (midori_location_action_entry_changed_cb), action); @@ -1145,6 +1345,10 @@ midori_location_action_connect_proxy (GtkAction* action, midori_location_action_changed_cb, action, "signal::key-press-event", midori_location_action_key_press_event_cb, action, + #if GTK_CHECK_VERSION (2, 19, 3) + "signal::preedit-changed", + midori_location_action_preedit_changed_cb, action, + #endif "signal::focus-in-event", midori_location_action_focus_in_event_cb, action, "signal::focus-out-event", -- 2.39.5