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.
This commit is contained in:
Christian Dywan 2010-01-17 17:45:34 +01:00
parent 27634e4f1e
commit 07261e284c

View file

@ -21,6 +21,7 @@
#include <glib/gi18n.h> #include <glib/gi18n.h>
#include <gdk/gdkkeysyms.h> #include <gdk/gdkkeysyms.h>
#define COMPLETION_DELAY 150
#define MAX_ITEMS 25 #define MAX_ITEMS 25
struct _MidoriLocationAction struct _MidoriLocationAction
@ -34,6 +35,12 @@ struct _MidoriLocationAction
GtkTreeModel* model; GtkTreeModel* model;
GtkTreeModel* filter_model; GtkTreeModel* filter_model;
guint completion_timeout;
gchar* key;
GtkWidget* popup;
GtkWidget* treeview;
GtkTreeModel* completion_model;
GtkWidget* entry;
GdkPixbuf* default_icon; GdkPixbuf* default_icon;
GHashTable* items; GHashTable* items;
KatzeNet* net; KatzeNet* net;
@ -109,9 +116,17 @@ static void
midori_location_action_disconnect_proxy (GtkAction* action, midori_location_action_disconnect_proxy (GtkAction* action,
GtkWidget* proxy); GtkWidget* proxy);
static gboolean
midori_location_entry_completion_match_cb (GtkTreeModel* model,
GtkTreeIter* iter,
gpointer data);
static void static void
midori_location_action_completion_init (MidoriLocationAction* location_action, midori_location_entry_render_text_cb (GtkCellLayout* layout,
GtkEntry* entry); GtkCellRenderer* renderer,
GtkTreeModel* model,
GtkTreeIter* iter,
gpointer data);
static void static void
midori_location_action_class_init (MidoriLocationActionClass* class) midori_location_action_class_init (MidoriLocationActionClass* class)
@ -247,6 +262,153 @@ midori_location_action_class_init (MidoriLocationActionClass* class)
"style \"midori-location-entry-style\"\n"); "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 */ /* Allow this to be used in tests, it's otherwise private */
/*static*/ GtkWidget* /*static*/ GtkWidget*
midori_location_action_entry_for_proxy (GtkWidget* proxy) 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)); entry = gtk_bin_get_child (GTK_BIN (location_entry));
g_object_set (location_entry, "model", model, NULL); 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 static void
midori_location_action_init (MidoriLocationAction* location_action) 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->progress = 0.0;
location_action->secondary_icon = NULL; location_action->secondary_icon = NULL;
location_action->default_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 (); 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->model, NULL);
katze_object_assign (location_action->filter_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); katze_object_assign (location_action->default_icon, NULL);
g_hash_table_destroy (location_action->items); g_hash_table_destroy (location_action->items);
@ -584,43 +746,138 @@ midori_location_action_key_press_event_cb (GtkEntry* entry,
GdkEventKey* event, GdkEventKey* event,
GtkAction* action) 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) switch (event->keyval)
{ {
case GDK_ISO_Enter: case GDK_ISO_Enter:
case GDK_KP_Enter: case GDK_KP_Enter:
case GDK_Return: case GDK_Return:
{ is_enter = TRUE;
if ((uri = gtk_entry_get_text (entry)) && *uri) 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, GtkTreeModel* model = location_action->completion_model;
(event->state & GDK_CONTROL_MASK) ? TRUE : FALSE); GtkTreeIter iter;
return TRUE; 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: 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); 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_Down:
case GDK_KP_Down:
case GDK_Up: 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")) if (!katze_object_get_boolean (parent, "popup-shown"))
gtk_combo_box_popup (GTK_COMBO_BOX (parent)); gtk_combo_box_popup (GTK_COMBO_BOX (parent));
return TRUE; return TRUE;
} }
case GDK_Page_Up: default:
case GDK_Page_Down: if ((text = gtk_entry_get_text (entry)) && *text)
{ {
return TRUE; midori_location_action_popup_completion (location_action, widget, "");
} selected = -1;
return FALSE;
}
} }
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 static gboolean
midori_location_action_focus_in_event_cb (GtkWidget* widget, midori_location_action_focus_in_event_cb (GtkWidget* widget,
GdkEventKey* event, GdkEventKey* event,
@ -635,6 +892,7 @@ midori_location_action_focus_out_event_cb (GtkWidget* widget,
GdkEventKey* event, GdkEventKey* event,
GtkAction* action) GtkAction* action)
{ {
midori_location_action_popdown_completion (MIDORI_LOCATION_ACTION (action));
g_signal_emit (action, signals[FOCUS_OUT], 0); g_signal_emit (action, signals[FOCUS_OUT], 0);
return FALSE; return FALSE;
} }
@ -775,17 +1033,15 @@ midori_location_entry_render_text_cb (GtkCellLayout* layout,
} }
static gboolean static gboolean
midori_location_entry_completion_match_cb (GtkEntryCompletion* completion, midori_location_action_match (GtkTreeModel* model,
const gchar* key, const gchar* key,
GtkTreeIter* iter, GtkTreeIter* iter,
gpointer data) gpointer data)
{ {
GtkTreeModel* model;
gchar* uri; gchar* uri;
gchar* title; gchar* title;
gboolean match; gboolean match;
model = gtk_entry_completion_get_model (completion);
gtk_tree_model_get (model, iter, URI_COL, &uri, TITLE_COL, &title, -1); gtk_tree_model_get (model, iter, URI_COL, &uri, TITLE_COL, &title, -1);
match = FALSE; match = FALSE;
@ -803,6 +1059,15 @@ midori_location_entry_completion_match_cb (GtkEntryCompletion* completion,
return match; 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: * midori_location_action_iter_lookup:
* @location_action: a #MidoriLocationAction * @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 static void
midori_location_action_entry_changed_cb (GtkComboBox* combo_box, midori_location_action_entry_changed_cb (GtkComboBox* combo_box,
MidoriLocationAction* location_action) MidoriLocationAction* location_action)
@ -1136,7 +1337,6 @@ midori_location_action_connect_proxy (GtkAction* action,
renderer, midori_location_entry_render_text_cb, child, NULL); renderer, midori_location_entry_render_text_cb, child, NULL);
gtk_combo_box_set_active (GTK_COMBO_BOX (entry), -1); 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_signal_connect (entry, "changed",
G_CALLBACK (midori_location_action_entry_changed_cb), action); 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, midori_location_action_changed_cb, action,
"signal::key-press-event", "signal::key-press-event",
midori_location_action_key_press_event_cb, action, 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", "signal::focus-in-event",
midori_location_action_focus_in_event_cb, action, midori_location_action_focus_in_event_cb, action,
"signal::focus-out-event", "signal::focus-out-event",