Updating branch refs/heads/master to bd1d4f8cc00cc62b826e2d786aed75b0275020dd (commit) from 2ffca48f81a5d4ed19a449428ba4c472f8317455 (commit)
commit bd1d4f8cc00cc62b826e2d786aed75b0275020dd Author: Christian Dywan <christ...@twotoasts.de> Date: Thu Sep 27 20:34:57 2012 +0200 Introduce Completion API with search and history classes The new API makes completion independent from the database and uses pluggable, asynchronous backends. No new functionality. extensions/wscript_build | 2 +- midori/midori-completion.vala | 152 ++++++++++++++++ midori/midori-historycompletion.vala | 113 ++++++++++++ midori/midori-locationaction.c | 331 ++++++++-------------------------- midori/midori-searchcompletion.vala | 77 ++++++++ midori/wscript_build | 2 +- po/POTFILES.in | 2 + tests/completion.vala | 85 +++++++++ tests/extensions.c | 1 + tests/wscript_build | 2 +- 10 files changed, 509 insertions(+), 258 deletions(-) diff --git a/extensions/wscript_build b/extensions/wscript_build index c09a92b..cf556c9 100644 --- a/extensions/wscript_build +++ b/extensions/wscript_build @@ -34,7 +34,7 @@ for extension in extensions: obj.source = source obj.uselib = 'UNIQUE LIBSOUP GIO GTK SQLITE WEBKIT LIBXML HILDON' obj.vapi_dirs = '../midori ../katze' - obj.packages = 'glib-2.0 gio-2.0 libsoup-2.4 midori midori-core katze' + obj.packages = 'glib-2.0 gio-2.0 libsoup-2.4 sqlite3 midori midori-core katze' if bld.env['HAVE_GTK3']: obj.packages += ' gtk+-3.0 webkitgtk-3.0' else: diff --git a/midori/midori-completion.vala b/midori/midori-completion.vala new file mode 100644 index 0000000..f3327f5 --- /dev/null +++ b/midori/midori-completion.vala @@ -0,0 +1,152 @@ +/* + Copyright (C) 2012 Christian Dywan <christ...@twotoasts.de> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +namespace Midori { + public class Suggestion : GLib.Object { + public string? uri { get; set; } + public string? markup { get; set; } + public bool use_markup { get; set; } + public string? background { get; set; } + public GLib.Icon? icon { get; set; } + public bool action { get; set; default = false; } + + public Suggestion (string? uri, string? markup, bool use_markup=false, + string? background=null, GLib.Icon? icon=null) { + + GLib.Object (uri: uri, markup: markup, use_markup: use_markup, + background: background, icon: icon); + } + } + + public abstract class Completion : GLib.Object { + public string? description { get; set; } + public int max_items { get; internal set; default = 25; } + internal int position { get; set; } + + public abstract void prepare (GLib.Object app); + public abstract bool can_complete (string prefix); + public abstract bool can_action (string action); + public abstract async List<Suggestion>? complete (string text, string? action, Cancellable cancellable); + } + + public class Autocompleter : GLib.Object { + private GLib.Object app; + private List<Completion> completions; + private int next_position; + public Gtk.ListStore model { get; private set; } + private bool need_to_clear = false; + private uint current_count = 0; + private Cancellable? cancellable = null; + + public enum Columns { + ICON, + URI, + MARKUP, + BACKGROUND, + YALIGN, + N + } + + public Autocompleter (GLib.Object app) { + this.app = app; + completions = new List<Completion> (); + next_position = 0; + model = new Gtk.ListStore (Columns.N, + typeof (Gdk.Pixbuf), typeof (string), typeof (string), + typeof (string), typeof (float)); + } + + public void add (Completion completion) { + completion.prepare (app); + completion.position = next_position; + next_position += completion.max_items; + completions.append (completion); + } + + public bool can_complete (string text) { + foreach (var completion in completions) + if (completion.can_complete (text)) + return true; + return false; + } + + private void fill_model (GLib.Object? object, AsyncResult result) { + var completion = object as Completion; + List<Suggestion>? suggestions = completion.complete.end (result); + if (suggestions == null) + return; + + if (need_to_clear) { + model.clear (); + need_to_clear = false; + current_count = 0; + } + + uint count = 0; + foreach (var suggestion in suggestions) { + model.insert_with_values (null, completion.position, + Columns.URI, suggestion.uri, + Columns.MARKUP, suggestion.use_markup + ? suggestion.markup : Markup.escape_text (suggestion.markup), + Columns.ICON, suggestion.icon, + Columns.BACKGROUND, suggestion.background, + Columns.YALIGN, 0.25); + + count++; + if (count > completion.max_items) + break; + } + + current_count += count; + populated (current_count); + } + + public signal void populated (uint count); + + public async void complete (string text) { + if (cancellable != null) + cancellable.cancel (); + cancellable = new Cancellable (); + need_to_clear = true; + + foreach (var completion in completions) { + if (completion.can_complete (text)) + completion.complete.begin (text, null, cancellable, fill_model); + + uint src = Idle.add (complete.callback); + yield; + Source.remove (src); + + if (cancellable.is_cancelled ()) + break; + } + } + + public bool can_action (string action) { + foreach (var completion in completions) + if (completion.can_action (action)) + return true; + return false; + } + + public async void action (string action, string text) { + if (cancellable != null) + cancellable.cancel (); + cancellable = new Cancellable (); + need_to_clear = true; + + foreach (var completion in completions) { + if (completion.can_action (action)) + completion.complete.begin (text, action, cancellable, fill_model); + } + } + } +} diff --git a/midori/midori-historycompletion.vala b/midori/midori-historycompletion.vala new file mode 100644 index 0000000..5a194d9 --- /dev/null +++ b/midori/midori-historycompletion.vala @@ -0,0 +1,113 @@ +/* + Copyright (C) 2012 Christian Dywan <christ...@twotoasts.de> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +namespace Katze { + extern static Gdk.Pixbuf? load_cached_icon (string uri, Gtk.Widget? proxy); +} + +namespace Midori { + public class HistoryCompletion : Completion { + unowned Sqlite.Database db; + + public HistoryCompletion () { + GLib.Object (description: "Bookmarks and history"); + } + + public override void prepare (GLib.Object app) { + GLib.Object history; + app.get ("history", out history); + return_if_fail (history != null); + db = history.get_data<Sqlite.Database?> ("db"); + return_if_fail (db != null); + } + + public override bool can_complete (string text) { + return db != null; + } + + public override bool can_action (string action) { + return false; + } + + public override async List<Suggestion>? complete (string text, string? action, Cancellable cancellable) { + return_val_if_fail (db != null, null); + + Sqlite.Statement stmt; + unowned string sqlcmd = """ + SELECT type, uri, title FROM ( + SELECT 1 AS type, uri, title, count() AS ct FROM history + WHERE uri LIKE ?1 OR title LIKE ?1 GROUP BY uri + UNION ALL + SELECT 2 AS type, replace(uri, '%s', keywords) AS uri, + keywords AS title, count() AS ct FROM search + WHERE uri LIKE ?1 OR title LIKE ?1 GROUP BY uri + UNION ALL + SELECT 1 AS type, uri, title, 50 AS ct FROM bookmarks + WHERE title LIKE ?1 OR uri LIKE ?1 AND uri !='' + ) GROUP BY uri ORDER BY ct DESC LIMIT ?2 + """; + if (db.prepare_v2 (sqlcmd, -1, out stmt, null) != Sqlite.OK) { + critical (_("Failed to initialize history: %s"), db.errmsg ()); + return null; + } + + string query = "%" + text.replace (" ", "%") + "%"; + stmt.bind_text (1, query); + stmt.bind_int64 (2, max_items); + + int result = stmt.step (); + if (result != Sqlite.ROW) { + if (result == Sqlite.ERROR) + critical (_("Failed to select from history: %s"), db.errmsg ()); + return null; + } + + var suggestions = new List<Suggestion> (); + while (result == Sqlite.ROW) { + int64 type = stmt.column_int64 (0); + unowned string uri = stmt.column_text (1); + unowned string title = stmt.column_text (2); + Gdk.Pixbuf? icon = Katze.load_cached_icon (uri, null); + + switch (type) { + case 1: /* history_view */ + var suggestion = new Suggestion (uri, title, false, null, icon); + suggestions.append (suggestion); + break; + case 2: /* search_view */ + string desc = _("Search for %s").printf (title) + "\n" + uri; + /* FIXME: Theming? Win32? */ + string background = "gray"; + var suggestion = new Suggestion (uri, desc, false, background, icon); + suggestions.append (suggestion); + break; + default: + warn_if_reached (); + break; + } + + uint src = Idle.add (complete.callback); + yield; + Source.remove (src); + + if (cancellable.is_cancelled ()) + return null; + + result = stmt.step (); + } + + if (cancellable.is_cancelled ()) + return null; + + return suggestions; + } + } +} diff --git a/midori/midori-locationaction.c b/midori/midori-locationaction.c index 49f3295..ad13677 100644 --- a/midori/midori-locationaction.c +++ b/midori/midori-locationaction.c @@ -15,6 +15,7 @@ #include "marshal.h" #include "midori-browser.h" #include "midori-searchaction.h" +#include "midori-app.h" #include "midori-platform.h" #include <midori/midori-core.h> @@ -25,9 +26,6 @@ #include <sqlite3.h> -#define COMPLETION_DELAY 200 -#define MAX_ITEMS 25 - struct _MidoriLocationAction { GtkAction parent_instance; @@ -37,14 +35,13 @@ struct _MidoriLocationAction gdouble progress; gchar* secondary_icon; - guint completion_timeout; gchar* key; + MidoriAutocompleter* autocompleter; GtkWidget* popup; GtkWidget* treeview; GtkTreeModel* completion_model; gint completion_index; GtkWidget* entry; - GdkPixbuf* default_icon; KatzeArray* history; }; @@ -77,19 +74,6 @@ enum static guint signals[LAST_SIGNAL]; -enum -{ - FAVICON_COL, - URI_COL, - TITLE_COL, - VISITS_COL, - VISIBLE_COL, - YALIGN_COL, - BACKGROUND_COL, - STYLE_COL, - N_COLS -}; - static void midori_location_action_finalize (GObject* object); @@ -265,16 +249,6 @@ midori_location_action_class_init (MidoriLocationActionClass* class) G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); } -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, - GDK_TYPE_COLOR, G_TYPE_BOOLEAN); - return model; -} - static void midori_location_action_popup_position (MidoriLocationAction* action, gint matches) @@ -290,10 +264,8 @@ midori_location_action_popup_position (MidoriLocationAction* action, GdkRectangle monitor; GtkAllocation alloc; gint height, sep, width, toplevel_height; - gboolean above; GtkWidget* scrolled = gtk_widget_get_parent (action->treeview); GtkWidget* toplevel; - GtkTreePath* path; if (!window) return; @@ -350,20 +322,9 @@ midori_location_action_popup_position (MidoriLocationAction* action, if (wy + widget_req.height + menu_req.height <= monitor.y + monitor.height || wy - monitor.y < (monitor.y + monitor.height) - (wy + widget_req.height)) - { wy += widget_req.height; - above = FALSE; - } else - { wy -= menu_req.height; - above = TRUE; - } - - path = gtk_tree_path_new_from_indices (above ? matches - 1 : 0, -1); - gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (action->treeview), path, - NULL, FALSE, 0.0, 0.0); - gtk_tree_path_free (path); gtk_window_move (GTK_WINDOW (popup), wx, wy); } @@ -394,77 +355,13 @@ midori_location_action_entry_set_text (GtkWidget* entry, } } -static int -midori_location_action_add_search_engines (MidoriLocationAction* action, - GtkListStore* store, - gint matches) -{ - KatzeItem* item; - gint i = 0; - #ifndef G_OS_WIN32 - GtkStyle* style; - #endif - - gtk_widget_realize (action->treeview); - #ifndef G_OS_WIN32 - style = gtk_widget_get_style (action->treeview); - #endif - - /* FIXME: choose 3 most frequently except for default */ - KATZE_ARRAY_FOREACH_ITEM (item, action->search_engines) - { - gchar* uri; - gchar* title; - const gchar* text; - gchar* desc; - GdkPixbuf* icon; - - uri = midori_uri_for_search (katze_item_get_uri (item), action->key); - title = g_strdup_printf (_("Search with %s"), katze_item_get_name (item)); - text = katze_item_get_text (item); - desc = g_strdup_printf ("%s\n%s", title, text ? text : uri); - icon = midori_search_action_get_icon (item, action->treeview, NULL, FALSE); - gtk_list_store_insert_with_values (store, NULL, matches + i, - URI_COL, uri, TITLE_COL, desc, YALIGN_COL, 0.25, - #ifndef G_OS_WIN32 - BACKGROUND_COL, style ? &style->bg[GTK_STATE_NORMAL] : NULL, - #endif - STYLE_COL, 1, FAVICON_COL, icon, -1); - g_free (uri); - g_free (title); - g_free (desc); - if (icon != NULL) - g_object_unref (icon); - i++; - - if (i > 2 && matches > 0) - { - gtk_list_store_insert_with_values (store, NULL, matches + i, - URI_COL, "about:search", TITLE_COL, _("Search with…"), - YALIGN_COL, 0.25, - #ifndef G_OS_WIN32 - BACKGROUND_COL, style ? &style->bg[GTK_STATE_NORMAL] : NULL, - #endif - STYLE_COL, 1, FAVICON_COL, NULL, -1); - i++; - break; - } - } - return i; -} - static void midori_location_action_complete (MidoriLocationAction* action, gboolean new_tab, const gchar* uri) { - if (!strcmp (uri, "about:search")) - { - GtkListStore* store = GTK_LIST_STORE (action->completion_model); - gtk_list_store_clear (store); - midori_location_action_popup_position (action, - midori_location_action_add_search_engines (action, store, 0)); - } + if (midori_autocompleter_can_action (action->autocompleter, uri)) + midori_autocompleter_action (action->autocompleter, uri, action->key, NULL, NULL); else { midori_location_action_popdown_completion (action); @@ -488,9 +385,10 @@ midori_location_action_treeview_button_press_cb (GtkWidget* treeview, gtk_tree_model_get_iter (action->completion_model, &iter, path); gtk_tree_path_free (path); - gtk_tree_model_get (action->completion_model, &iter, URI_COL, &uri, -1); + gtk_tree_model_get (action->completion_model, &iter, + MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1); midori_location_action_complete (action, - MIDORI_MOD_NEW_TAB (event->state), uri); + MIDORI_MOD_NEW_TAB (event->state), uri); g_free (uri); return TRUE; @@ -499,29 +397,26 @@ midori_location_action_treeview_button_press_cb (GtkWidget* treeview, return FALSE; } +static void +midori_location_action_populated_suggestions_cb (MidoriAutocompleter* autocompleter, + guint count, + MidoriLocationAction* action) +{ + GtkTreePath* path = gtk_tree_path_new_first (); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (action->treeview), path, NULL, + FALSE, 0.0, 0.0); + gtk_tree_path_free (path); + midori_location_action_popup_position (action, count); +} + static gboolean midori_location_action_popup_timeout_cb (gpointer data) { MidoriLocationAction* action = data; GtkTreeViewColumn* column; - GtkListStore* store; - gchar* effective_key; - gint i; - gint result; - static sqlite3_stmt* stmt; - const gchar* sqlcmd; - gint matches, searches; - - if (!action->entry || !gtk_widget_has_focus (action->entry) || !action->history) - return FALSE; - /* No completion when typing a search token */ - if (action->search_engines - && katze_array_find_token (action->search_engines, action->key)) - { - midori_location_action_popdown_completion (action); + if (!gtk_widget_has_focus (action->entry)) return FALSE; - } /* Empty string or starting with a space means: no completion */ if (!(action->key && *action->key && *action->key != ' ')) @@ -530,61 +425,40 @@ midori_location_action_popup_timeout_cb (gpointer data) return FALSE; } - if (!stmt) - { - sqlite3* db; - db = g_object_get_data (G_OBJECT (action->history), "db"); - - if (!db) - return FALSE; - - sqlcmd = "SELECT type, uri, title FROM (" - " SELECT 1 AS type, uri, title, count() AS ct FROM history " - " WHERE uri LIKE ?1 OR title LIKE ?1 GROUP BY uri " - " UNION ALL " - " SELECT 2 AS type, replace(uri, '%s', keywords) AS uri, " - " keywords AS title, count() AS ct FROM search " - " WHERE uri LIKE ?1 OR title LIKE ?1 GROUP BY uri " - " UNION ALL " - " SELECT 1 AS type, uri, title, 50 AS ct FROM bookmarks " - " WHERE title LIKE ?1 OR uri LIKE ?1 AND uri !='' " - ") GROUP BY uri ORDER BY ct DESC LIMIT ?2"; - sqlite3_prepare_v2 (db, sqlcmd, strlen (sqlcmd) + 1, &stmt, NULL); - } - effective_key = g_strdup_printf ("%%%s%%", action->key); - i = 0; - do + if (action->autocompleter == NULL) { - if (effective_key[i] == ' ') - effective_key[i] = '%'; - i++; + /* FIXME: use a real app here */ + GObject* app = g_object_new (MIDORI_TYPE_APP, + "history", action->history, + "search-engines", action->search_engines, + NULL); + action->autocompleter = midori_autocompleter_new (app); + g_signal_connect (action->autocompleter, "populated", + G_CALLBACK (midori_location_action_populated_suggestions_cb), action); + g_object_unref (app); + midori_autocompleter_add (action->autocompleter, + MIDORI_COMPLETION (midori_history_completion_new ())); + midori_autocompleter_add (action->autocompleter, + MIDORI_COMPLETION (midori_search_completion_new ())); } - while (effective_key[i] != '\0'); - sqlite3_bind_text (stmt, 1, effective_key, -1, g_free); - sqlite3_bind_int64 (stmt, 2, MAX_ITEMS); - result = sqlite3_step (stmt); - if (result != SQLITE_ROW && !action->search_engines) + if (!midori_autocompleter_can_complete (action->autocompleter, action->key)) { - if (result == SQLITE_ERROR) - g_print (_("Failed to select from history\n")); - sqlite3_reset (stmt); - sqlite3_clear_bindings (stmt); midori_location_action_popdown_completion (action); return FALSE; } + midori_autocompleter_complete (action->autocompleter, action->key, NULL, NULL); + if (G_UNLIKELY (!action->popup)) { - GtkTreeModel* model = NULL; GtkWidget* popup; GtkWidget* popup_frame; GtkWidget* scrolled; GtkWidget* treeview; GtkCellRenderer* renderer; - model = midori_location_action_create_model (); - action->completion_model = model; + action->completion_model = (GtkTreeModel*)midori_autocompleter_get_model (action->autocompleter); popup = gtk_window_new (GTK_WINDOW_POPUP); gtk_window_set_type_hint (GTK_WINDOW (popup), GDK_WINDOW_TYPE_HINT_COMBO); @@ -597,7 +471,7 @@ midori_location_action_popup_timeout_cb (gpointer data) "hscrollbar-policy", GTK_POLICY_NEVER, "vscrollbar-policy", GTK_POLICY_AUTOMATIC, NULL); gtk_container_add (GTK_CONTAINER (popup_frame), scrolled); - treeview = gtk_tree_view_new_with_model (model); + treeview = gtk_tree_view_new_with_model (action->completion_model); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE); gtk_tree_view_set_hover_selection (GTK_TREE_VIEW (treeview), TRUE); gtk_container_add (GTK_CONTAINER (scrolled), treeview); @@ -607,22 +481,25 @@ midori_location_action_popup_timeout_cb (gpointer data) gtk_widget_set_size_request (gtk_scrolled_window_get_vscrollbar ( GTK_SCROLLED_WINDOW (scrolled)), -1, 0); action->treeview = treeview; + #if !GTK_CHECK_VERSION (3, 4, 0) + gtk_widget_realize (action->treeview); + #endif 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, - "cell-background-gdk", BACKGROUND_COL, + "pixbuf", MIDORI_AUTOCOMPLETER_COLUMNS_ICON, + "yalign", MIDORI_AUTOCOMPLETER_COLUMNS_YALIGN, + "cell-background", MIDORI_AUTOCOMPLETER_COLUMNS_BACKGROUND, 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_attributes (GTK_CELL_LAYOUT (column), renderer, - "cell-background-gdk", BACKGROUND_COL, + "cell-background", MIDORI_AUTOCOMPLETER_COLUMNS_BACKGROUND, NULL); gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), renderer, midori_location_entry_render_text_cb, @@ -632,63 +509,21 @@ midori_location_action_popup_timeout_cb (gpointer data) action->popup = popup; g_signal_connect (popup, "destroy", G_CALLBACK (gtk_widget_destroyed), &action->popup); + gtk_widget_show_all (popup_frame); } - store = GTK_LIST_STORE (action->completion_model); - gtk_list_store_clear (store); - - matches = searches = 0; - while (result == SQLITE_ROW) - { - sqlite3_int64 type = sqlite3_column_int64 (stmt, 0); - const unsigned char* uri = sqlite3_column_text (stmt, 1); - const unsigned char* title = sqlite3_column_text (stmt, 2); - GdkPixbuf* icon = katze_load_cached_icon ((gchar*)uri, NULL); - if (!icon) - icon = g_object_ref (action->default_icon); - if (type == 1 /* history_view */) - { - gtk_list_store_insert_with_values (store, NULL, matches, - URI_COL, uri, TITLE_COL, title, YALIGN_COL, 0.25, - FAVICON_COL, icon, -1); - } - else if (type == 2 /* search_view */) - { - gchar* search_title = g_strdup_printf (_("Search for %s"), title); - gchar* search_desc = g_strdup_printf ("%s\n%s", search_title, uri); - gtk_list_store_insert_with_values (store, NULL, matches, - URI_COL, uri, TITLE_COL, search_desc, YALIGN_COL, 0.25, - STYLE_COL, 1, FAVICON_COL, icon, -1); - g_free (search_desc); - g_free (search_title); - } - if (icon != NULL) - g_object_unref (icon); - - matches++; - result = sqlite3_step (stmt); - } - - if (stmt) - { - sqlite3_reset (stmt); - sqlite3_clear_bindings (stmt); - } - - if (action->search_engines) - searches += midori_location_action_add_search_engines (action, store, matches); - if (!gtk_widget_get_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); + #if GTK_CHECK_VERSION (3, 4, 0) + gtk_window_set_attached_to (GTK_WINDOW (action->popup), action->entry); + #endif + gtk_widget_show (action->popup); } - midori_location_action_popup_position (action, matches + searches); - return FALSE; } @@ -697,14 +532,14 @@ midori_location_action_popup_completion (MidoriLocationAction* action, GtkWidget* entry, gchar* key) { - if (action->completion_timeout) - g_source_remove (action->completion_timeout); katze_assign (action->key, 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); + if (action->entry != entry) + { + action->entry = entry; + g_signal_connect (entry, "destroy", + G_CALLBACK (gtk_widget_destroyed), &action->entry); + } + g_idle_add (midori_location_action_popup_timeout_cb, action); } static void @@ -717,11 +552,6 @@ midori_location_action_popdown_completion (MidoriLocationAction* location_action gtk_tree_selection_unselect_all (gtk_tree_view_get_selection ( GTK_TREE_VIEW (location_action->treeview))); } - if (location_action->completion_timeout) - { - g_source_remove (location_action->completion_timeout); - location_action->completion_timeout = 0; - } location_action->completion_index = -1; } @@ -737,17 +567,8 @@ midori_location_action_entry_for_proxy (GtkWidget* proxy) static void midori_location_action_init (MidoriLocationAction* location_action) { - location_action->text = NULL; - location_action->search_engines = NULL; location_action->progress = 0.0; - location_action->secondary_icon = NULL; - location_action->default_icon = NULL; - location_action->completion_timeout = 0; location_action->completion_index = -1; - location_action->key = NULL; - location_action->popup = NULL; - location_action->entry = NULL; - location_action->history = NULL; } static void @@ -756,7 +577,8 @@ midori_location_action_finalize (GObject* object) MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (object); katze_assign (location_action->text, NULL); - katze_assign (location_action->search_engines, NULL); + katze_object_assign (location_action->search_engines, NULL); + katze_assign (location_action->autocompleter, NULL); katze_assign (location_action->key, NULL); if (location_action->popup) @@ -764,7 +586,6 @@ midori_location_action_finalize (GObject* object) gtk_widget_destroy (location_action->popup); location_action->popup = NULL; } - katze_object_assign (location_action->default_icon, NULL); katze_object_assign (location_action->history, NULL); G_OBJECT_CLASS (midori_location_action_parent_class)->finalize (object); @@ -981,7 +802,8 @@ midori_location_action_key_press_event_cb (GtkEntry* entry, gtk_tree_model_iter_nth_child (model, &iter, NULL, selected)) { gchar* uri; - gtk_tree_model_get (model, &iter, URI_COL, &uri, -1); + gtk_tree_model_get (model, &iter, + MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1); if (is_enter) midori_location_action_complete (location_action, @@ -1032,7 +854,8 @@ midori_location_action_key_press_event_cb (GtkEntry* entry, gchar* errmsg; gint result; - gtk_tree_model_get (model, &iter, URI_COL, &uri, -1); + gtk_tree_model_get (model, &iter, + MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1); sqlcmd = sqlite3_mprintf ("DELETE FROM history " "WHERE uri = '%q'", uri); g_free (uri); @@ -1042,7 +865,7 @@ midori_location_action_key_press_event_cb (GtkEntry* entry, if (result == SQLITE_ERROR) { gtk_list_store_set (GTK_LIST_STORE (model), &iter, - URI_COL, errmsg, -1); + MIDORI_AUTOCOMPLETER_COLUMNS_URI, errmsg, -1); sqlite3_free (errmsg); break; } @@ -1126,7 +949,8 @@ midori_location_action_key_press_event_cb (GtkEntry* entry, if (gtk_tree_model_iter_nth_child (model, &iter, NULL, selected)) { gchar* uri; - gtk_tree_model_get (model, &iter, URI_COL, &uri, -1); + gtk_tree_model_get (model, &iter, + MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1); /* Update the layout without actually changing the text */ pango_layout_set_text (gtk_entry_get_layout (entry), uri, -1); g_free (uri); @@ -1449,7 +1273,7 @@ midori_location_entry_render_text_cb (GtkCellLayout* layout, gchar* uri_temp; gchar* uri; gchar* title; - gboolean style; + gchar* background; gchar* desc; gchar* desc_uri; gchar* desc_iter; @@ -1467,15 +1291,19 @@ midori_location_entry_render_text_cb (GtkCellLayout* layout, gchar** parts; size_t offset; - gtk_tree_model_get (model, iter, URI_COL, &uri_escaped, TITLE_COL, &title, - STYLE_COL, &style, -1); + gtk_tree_model_get (model, iter, + MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri_escaped, + MIDORI_AUTOCOMPLETER_COLUMNS_MARKUP, &title, + MIDORI_AUTOCOMPLETER_COLUMNS_BACKGROUND, &background, + -1); - if (style) /* A search engine action */ + if (background != NULL) /* A search engine or action suggestion */ { - g_object_set (renderer, "text", title, + g_object_set (renderer, "markup", title, "ellipsize-set", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); g_free (uri_escaped); g_free (title); + g_free (background); return; } @@ -1685,16 +1513,9 @@ static void midori_location_action_connect_proxy (GtkAction* action, GtkWidget* proxy) { - MidoriLocationAction* location_action; - GTK_ACTION_CLASS (midori_location_action_parent_class)->connect_proxy ( action, proxy); - location_action = MIDORI_LOCATION_ACTION (action); - katze_object_assign (location_action->default_icon, - gtk_widget_render_icon (proxy, GTK_STOCK_FILE, - GTK_ICON_SIZE_MENU, NULL)); - if (GTK_IS_TOOL_ITEM (proxy)) { GtkWidget* entry = midori_location_action_entry_for_proxy (proxy); diff --git a/midori/midori-searchcompletion.vala b/midori/midori-searchcompletion.vala new file mode 100644 index 0000000..0a5c44e --- /dev/null +++ b/midori/midori-searchcompletion.vala @@ -0,0 +1,77 @@ +/* + Copyright (C) 2012 Christian Dywan <christ...@twotoasts.de> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ +namespace Katze { + extern static unowned List<GLib.Object> array_peek_items (GLib.Object array); +} + +namespace Midori { + extern static Gdk.Pixbuf? search_action_get_icon (GLib.Object item, + Gtk.Widget? widget, out string[] icon_name, bool in_entry); + + public class SearchCompletion : Completion { + GLib.Object search_engines; + + public SearchCompletion () { + GLib.Object (description: "Search engines"); + } + + public override void prepare (GLib.Object app) { + app.get ("search-engines", out search_engines); + } + + public override bool can_complete (string text) { + return search_engines != null; + } + + public override bool can_action (string action) { + return action == "about:search"; + } + + public override async List<Suggestion>? complete (string text, string? action, Cancellable cancellable) { + return_val_if_fail (search_engines != null, null); + + unowned List<GLib.Object> items = Katze.array_peek_items (search_engines); + var suggestions = new List<Suggestion> (); + uint n = 0; + foreach (var item in items) { + string uri, title, desc; + item.get ("uri", out uri); + item.get ("name", out title); + item.get ("text", out desc); + string search_uri = URI.for_search (uri, text); + string search_title = _("Search with %s").printf (title); + var icon = Midori.search_action_get_icon (item, null, null, false); + string search_desc = search_title + "\n" + desc ?? uri; + /* FIXME: Theming? Win32? */ + string background = "gray"; + var suggestion = new Suggestion (search_uri, search_desc, false, background, icon); + suggestions.append (suggestion); + + n++; + if (n == 3 && action == null) { + suggestion = new Suggestion ("about:search", _("Search with…"), false, background); + suggestion.action = true; + suggestions.append (suggestion); + break; + } + + uint src = Idle.add (complete.callback); + yield; + Source.remove (src); + } + + if (cancellable.is_cancelled ()) + return null; + + return suggestions; + } + } +} diff --git a/midori/wscript_build b/midori/wscript_build index 52c3337..6c8c0e8 100644 --- a/midori/wscript_build +++ b/midori/wscript_build @@ -19,7 +19,7 @@ if progressive: obj.add_marshal_file ('marshal.list', 'midori_cclosure_marshal') obj.install_path = None obj.vapi_dirs = '../midori ../katze' - obj.packages = 'glib-2.0 gio-2.0 libsoup-2.4 posix' + obj.packages = 'glib-2.0 gio-2.0 libsoup-2.4 posix sqlite3' if bld.env['HAVE_GTK3']: obj.packages += ' gtk+-3.0 webkitgtk-3.0' else: diff --git a/po/POTFILES.in b/po/POTFILES.in index d0688b6..9f4b2d9 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -17,6 +17,8 @@ midori/midori-download.vala midori/midori-speeddial.vala midori/midori-preferences.c midori/midori-searchaction.c +midori/midori-historycompletion.vala +midori/midori-searchcompletion.vala midori/sokoke.c toolbars/midori-findbar.c toolbars/midori-transferbar.c diff --git a/tests/completion.vala b/tests/completion.vala new file mode 100644 index 0000000..98d7d99 --- /dev/null +++ b/tests/completion.vala @@ -0,0 +1,85 @@ +/* + Copyright (C) 2012 Christian Dywan <christ...@twotoasts.de> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. +*/ + +class TestCompletion : Midori.Completion { + public bool test_can_complete { get; set; } + + public TestCompletion () { + } + + public override void prepare (GLib.Object app) { + } + + public override bool can_complete (string text) { + return test_can_complete; + } + + public override bool can_action (string action) { + return false; + } + + public override async List<Midori.Suggestion>? complete (string text, string? action, Cancellable cancellable) { + return null; + } +} + +void completion_autocompleter () { + var app = new Midori.App (); + var autocompleter = new Midori.Autocompleter (app); + assert (!autocompleter.can_complete ("")); + var completion = new TestCompletion (); + autocompleter.add (completion); + completion.test_can_complete = false; + assert (!autocompleter.can_complete ("")); + completion.test_can_complete = true; + assert (autocompleter.can_complete ("")); +} + +struct TestCaseCompletion { + public string prefix; + public string text; + public int expected_count; +} + +const TestCaseCompletion[] completions = { + { "history", "example", 1 } +}; + +async void complete_spec (Midori.Completion completion, TestCaseCompletion spec) { + assert (completion.can_complete (spec.text)); + var cancellable = new Cancellable (); + var suggestions = yield completion.complete (spec.text, null, cancellable); + if (spec.expected_count != suggestions.length ()) + error ("%u expected for %s/ %s but got %u", + spec.expected_count, spec.prefix, spec.text, suggestions.length ()); +} + +void completion_history () { + /* TODO: mock history database + var completion = new Midori.HistoryCompletion (); + var app = new Midori.App (); + var history = new Katze.Array (typeof (Katze.Item)); + app.set ("history", history); + Sqlite.Database db; + Sqlite.Database.open_v2 (":memory:", out db); + history.set_data<unowned Sqlite.Database?> ("db", db); + completion.prepare (app); + foreach (var spec in completions) + complete_spec (completion, spec); */ +} + +void main (string[] args) { + Test.init (ref args); + Test.add_func ("/completion/autocompleter", completion_autocompleter); + Test.add_func ("/completion/history", completion_history); + Test.run (); +} + diff --git a/tests/extensions.c b/tests/extensions.c index 97f85af..56bec50 100644 --- a/tests/extensions.c +++ b/tests/extensions.c @@ -169,6 +169,7 @@ extension_activate (gconstpointer data) MidoriApp* app = midori_app_new (); g_object_set (app, "settings", midori_web_settings_new (), NULL); midori_extension_activate (G_OBJECT (data), NULL, TRUE, app); + /* TODO: MidoriCompletion */ g_object_unref (app); } diff --git a/tests/wscript_build b/tests/wscript_build index 28b8c0c..85b0016 100644 --- a/tests/wscript_build +++ b/tests/wscript_build @@ -34,7 +34,7 @@ for test in tests: obj.cflags = ['-DEXTENSION_PATH="' + os.path.abspath ('_build/default/extensions') + '"'] obj.source = source obj.vapi_dirs = '../midori ../katze' - obj.packages = 'glib-2.0 gio-2.0 libsoup-2.4 katze midori midori-core' + obj.packages = 'glib-2.0 gio-2.0 libsoup-2.4 sqlite3 katze midori midori-core' if bld.env['HAVE_GTK3']: obj.packages += ' gtk+-3.0 webkitgtk-3.0' else: _______________________________________________ Xfce4-commits mailing list Xfce4-commits@xfce.org https://mail.xfce.org/mailman/listinfo/xfce4-commits