From: Alexander Butenko Date: Sat, 7 Jan 2012 05:31:37 +0000 (-0400) Subject: Formhistory 2.0 with GDOM frontend X-Git-Url: https://spindle.queued.net/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e61743db9c147049f86f0a4acdf60d8bd729f9b4;p=midori Formhistory 2.0 with GDOM frontend --- diff --git a/extensions/formhistory.c b/extensions/formhistory.c deleted file mode 100644 index 00e9ea72..00000000 --- a/extensions/formhistory.c +++ /dev/null @@ -1,600 +0,0 @@ -/* - Copyright (C) 2009 Alexander Butenko - Copyright (C) 2009 Christian Dywan - - 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. -*/ - -#define MAXCHARS 60 -#define MINCHARS 2 - -#include -#include - -#include "config.h" -#if HAVE_UNISTD_H - #include -#endif - -static GHashTable* global_keys; -static gchar* jsforms; - - -static void -formhistory_toggle_state_cb (GtkAction* action, - MidoriBrowser* browser); - -static gboolean -formhistory_prepare_js () -{ - gchar* autosuggest; - gchar* style; - guint i; - gchar* file; - - file = sokoke_find_data_filename ("autosuggestcontrol.js", TRUE); - if (!g_file_get_contents (file, &autosuggest, NULL, NULL)) - { - g_free (file); - return FALSE; - } - g_strchomp (autosuggest); - - katze_assign (file, sokoke_find_data_filename ("autosuggestcontrol.css", TRUE)); - if (!g_file_get_contents (file, &style, NULL, NULL)) - { - g_free (file); - return FALSE; - } - g_strchomp (style); - i = 0; - while (style[i]) - { - if (style[i] == '\n') - style[i] = ' '; - i++; - } - - jsforms = g_strdup_printf ( - "%s" - "window.addEventListener ('DOMContentLoaded'," - "function () {" - " if (document.getElementById('formhistory'))" - " return;" - " if (!initSuggestions ())" - " return;" - " var mystyle = document.createElement('style');" - " mystyle.setAttribute('type', 'text/css');" - " mystyle.setAttribute('id', 'formhistory');" - " mystyle.appendChild(document.createTextNode('%s'));" - " var head = document.getElementsByTagName('head')[0];" - " if (head) head.appendChild(mystyle);" - "}, true);", - autosuggest, - style); - g_strstrip (jsforms); - g_free (file); - g_free (style); - g_free (autosuggest); - return TRUE; -} - -static gchar* -formhistory_fixup_value (char* value) -{ - guint i = 0; - g_strchomp (value); - while (value[i]) - { - if (value[i] == '\n') - value[i] = ' '; - else if (value[i] == '"') - value[i] = '\''; - i++; - } - return value; -} - -static gchar* -formhistory_build_js () -{ - GString* suggestions; - GHashTableIter iter; - gpointer key, value; - - suggestions = g_string_new ( - "function FormSuggestions(eid) { " - "arr = new Array();"); - g_hash_table_iter_init (&iter, global_keys); - while (g_hash_table_iter_next (&iter, &key, &value)) - { - g_string_append_printf (suggestions, " arr[\"%s\"] = [%s]; ", - (gchar*)key, (gchar*)value); - } - g_string_append (suggestions, "this.suggestions = arr[eid]; }"); - g_string_append (suggestions, jsforms); - return g_string_free (suggestions, FALSE); -} - -static void -formhistory_update_database (gpointer db, - const gchar* key, - const gchar* value) -{ - gchar* sqlcmd; - gchar* errmsg; - gint success; - - sqlcmd = sqlite3_mprintf ("INSERT INTO forms VALUES" - "('%q', '%q', '%q')", - NULL, key, value); - success = sqlite3_exec (db, sqlcmd, NULL, NULL, &errmsg); - sqlite3_free (sqlcmd); - if (success != SQLITE_OK) - { - g_printerr (_("Failed to add form value: %s\n"), errmsg); - g_free (errmsg); - return; - } -} - -static gboolean -formhistory_update_main_hash (gchar* key, - gchar* value) -{ - guint length; - gchar* tmp; - - if (!(value && *value)) - return FALSE; - length = strlen (value); - if (length > MAXCHARS || length < MINCHARS) - return FALSE; - - formhistory_fixup_value (key); - formhistory_fixup_value (value); - if ((tmp = g_hash_table_lookup (global_keys, (gpointer)key))) - { - gchar* rvalue = g_strdup_printf ("\"%s\"",value); - gchar* patt = g_regex_escape_string (rvalue, -1); - if (!g_regex_match_simple (patt, tmp, - G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY)) - { - gchar* new_value = g_strdup_printf ("%s%s,", tmp, rvalue); - g_hash_table_insert (global_keys, g_strdup (key), new_value); - g_free (rvalue); - g_free (patt); - } - else - { - g_free (rvalue); - g_free (patt); - return FALSE; - } - } - else - { - gchar* new_value = g_strdup_printf ("\"%s\",",value); - g_hash_table_replace (global_keys, g_strdup (key), new_value); - } - return TRUE; -} - -static gboolean -formhistory_navigation_decision_cb (WebKitWebView* web_view, - WebKitWebFrame* web_frame, - WebKitNetworkRequest* request, - WebKitWebNavigationAction* action, - WebKitWebPolicyDecision* decision, - MidoriExtension* extension) -{ - /* The script returns form data in the form "field_name|,|value|,|field_type". - We are handling only input fields with 'text' or 'password' type. - The field separator is "|||" */ - const gchar* script = "function dumpForm (inputs) {" - " var out = '';" - " for (i=0;iF"); - gtk_action_set_accel_group (action, acg); - gtk_action_connect_accelerator (action); - - if (midori_extension_get_boolean (extension, "always-load")) - { - midori_browser_foreach (browser, - (GtkCallback)formhistory_add_tab_foreach_cb, extension); - g_signal_connect (browser, "add-tab", - G_CALLBACK (formhistory_add_tab_cb), extension); - } - g_signal_connect (extension, "deactivate", - G_CALLBACK (formhistory_deactivate_cb), browser); -} - -static void -formhistory_deactivate_tabs (MidoriView* view, - MidoriBrowser* browser, - MidoriExtension* extension) -{ - GtkWidget* web_view = midori_view_get_web_view (view); - g_signal_handlers_disconnect_by_func ( - web_view, formhistory_window_object_cleared_cb, NULL); - g_signal_handlers_disconnect_by_func ( - web_view, formhistory_navigation_decision_cb, extension); -} - -static void -formhistory_deactivate_cb (MidoriExtension* extension, - MidoriBrowser* browser) -{ - MidoriApp* app = midori_extension_get_app (extension); - sqlite3* db; - - GtkActionGroup* action_group = midori_browser_get_action_group (browser); - GtkAction* action; - - g_signal_handlers_disconnect_by_func ( - browser, formhistory_add_tab_cb, extension); - g_signal_handlers_disconnect_by_func ( - extension, formhistory_deactivate_cb, browser); - g_signal_handlers_disconnect_by_func ( - app, formhistory_app_add_browser_cb, extension); - midori_browser_foreach (browser, - (GtkCallback)formhistory_deactivate_tabs, extension); - - g_object_set_data (G_OBJECT (browser), "FormHistoryExtension", NULL); - action = gtk_action_group_get_action ( action_group, "FormHistoryToggleState"); - if (action != NULL) - { - gtk_action_group_remove_action (action_group, action); - g_object_unref (action); - } - - katze_assign (jsforms, NULL); - if (global_keys) - g_hash_table_destroy (global_keys); - - if ((db = g_object_get_data (G_OBJECT (extension), "formhistory-db"))) - sqlite3_close (db); -} - -static int -formhistory_add_field (gpointer data, - int argc, - char** argv, - char** colname) -{ - gint i; - gint ncols = 3; - - /* Test whether have the right number of columns */ - g_return_val_if_fail (argc % ncols == 0, 1); - - for (i = 0; i < (argc - ncols) + 1; i++) - { - if (argv[i]) - { - if (colname[i] && !g_ascii_strcasecmp (colname[i], "domain") - && colname[i + 1] && !g_ascii_strcasecmp (colname[i + 1], "field") - && colname[i + 2] && !g_ascii_strcasecmp (colname[i + 2], "value")) - { - gchar* key = argv[i + 1]; - formhistory_update_main_hash (g_strdup (key), g_strdup (argv[i + 2])); - } - } - } - return 0; -} - -static void -formhistory_activate_cb (MidoriExtension* extension, - MidoriApp* app) -{ - const gchar* config_dir; - gchar* filename; - sqlite3* db; - char* errmsg = NULL, *errmsg2 = NULL; - KatzeArray* browsers; - MidoriBrowser* browser; - - global_keys = g_hash_table_new_full (g_str_hash, g_str_equal, - (GDestroyNotify)g_free, - (GDestroyNotify)g_free); - if(!jsforms) - formhistory_prepare_js (); - config_dir = midori_extension_get_config_dir (extension); - katze_mkdir_with_parents (config_dir, 0700); - filename = g_build_filename (config_dir, "forms.db", NULL); - if (sqlite3_open (filename, &db) != SQLITE_OK) - { - /* If the folder is /, this is a test run, thus no error */ - if (!g_str_equal (midori_extension_get_config_dir (extension), "/")) - g_warning (_("Failed to open database: %s\n"), sqlite3_errmsg (db)); - sqlite3_close (db); - } - g_free (filename); - if ((sqlite3_exec (db, "CREATE TABLE IF NOT EXISTS " - "forms (domain text, field text, value text)", - NULL, NULL, &errmsg) == SQLITE_OK) - && (sqlite3_exec (db, "SELECT domain, field, value FROM forms ", - formhistory_add_field, - NULL, &errmsg2) == SQLITE_OK)) - g_object_set_data (G_OBJECT (extension), "formhistory-db", db); - else - { - if (errmsg) - { - g_critical (_("Failed to execute database statement: %s\n"), errmsg); - sqlite3_free (errmsg); - if (errmsg2) - { - g_critical (_("Failed to execute database statement: %s\n"), errmsg2); - sqlite3_free (errmsg2); - } - } - sqlite3_close (db); - } - - browsers = katze_object_get_object (app, "browsers"); - KATZE_ARRAY_FOREACH_ITEM (browser, browsers) - formhistory_app_add_browser_cb (app, browser, extension); - g_signal_connect (app, "add-browser", - G_CALLBACK (formhistory_app_add_browser_cb), extension); - - g_object_unref (browsers); -} - -static void -formhistory_preferences_response_cb (GtkWidget* dialog, - gint response_id, - MidoriExtension* extension) -{ - GtkWidget* checkbox; - gboolean old_state; - gboolean new_state; - MidoriApp* app; - KatzeArray* browsers; - MidoriBrowser* browser; - - if (response_id == GTK_RESPONSE_APPLY) - { - checkbox = g_object_get_data (G_OBJECT (dialog), "always-load-checkbox"); - new_state = !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox)); - old_state = midori_extension_get_boolean (extension, "always-load"); - - if (old_state != new_state) - { - midori_extension_set_boolean (extension, "always-load", new_state); - - app = midori_extension_get_app (extension); - browsers = katze_object_get_object (app, "browsers"); - KATZE_ARRAY_FOREACH_ITEM (browser, browsers) - { - midori_browser_foreach (browser, - (GtkCallback)formhistory_deactivate_tabs, extension); - g_signal_handlers_disconnect_by_func ( - browser, formhistory_add_tab_cb, extension); - - if (new_state) - { - midori_browser_foreach (browser, - (GtkCallback)formhistory_add_tab_foreach_cb, extension); - g_signal_connect (browser, "add-tab", - G_CALLBACK (formhistory_add_tab_cb), extension); - } - } - } - } - gtk_widget_destroy (dialog); -} - -static void -formhistory_preferences_cb (MidoriExtension* extension) -{ - GtkWidget* dialog; - GtkWidget* content_area; - GtkWidget* checkbox; - - dialog = gtk_dialog_new (); - - gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); - - gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); - gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_APPLY, GTK_RESPONSE_APPLY); - - content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); - checkbox = gtk_check_button_new_with_label (_("only activate form history via hotkey (Ctrl+Shift+F) per tab")); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (checkbox), - !midori_extension_get_boolean (extension, "always-load")); - g_object_set_data (G_OBJECT (dialog), "always-load-checkbox", checkbox); - gtk_container_add (GTK_CONTAINER (content_area), checkbox); - - g_signal_connect (dialog, - "response", - G_CALLBACK (formhistory_preferences_response_cb), - extension); - gtk_widget_show_all (dialog); -} - -static void -formhistory_toggle_state_cb (GtkAction* action, - MidoriBrowser* browser) -{ - MidoriView* view = MIDORI_VIEW (midori_browser_get_current_tab (browser)); - MidoriExtension* extension = g_object_get_data (G_OBJECT (browser), "FormHistoryExtension"); - GtkWidget* web_view = midori_view_get_web_view (view); - - if (g_signal_handler_find (web_view, G_SIGNAL_MATCH_FUNC, - g_signal_lookup ("window-object-cleared", MIDORI_TYPE_VIEW), 0, NULL, - formhistory_window_object_cleared_cb, extension)) - { - formhistory_deactivate_tabs (view, browser, extension); - } else { - formhistory_add_tab_cb (browser, view, extension); - } -} - - -#if G_ENABLE_DEBUG -/* - - - autosuggest testcase - - -
-

-

- -
- - */ -#endif - -MidoriExtension* -extension_init (void) -{ - gboolean should_init = TRUE; - const gchar* ver; - gchar* desc; - MidoriExtension* extension; - - if (formhistory_prepare_js ()) - { - ver = "1.0" MIDORI_VERSION_SUFFIX; - desc = g_strdup (_("Stores history of entered form data")); - } - else - { - desc = g_strdup_printf (_("Not available: %s"), - _("Resource files not installed")); - ver = NULL; - should_init = FALSE; - } - - extension = g_object_new (MIDORI_TYPE_EXTENSION, - "name", _("Form history filler"), - "description", desc, - "version", ver, - "authors", "Alexander V. Butenko ", - NULL); - - g_free (desc); - - if (should_init) - { - midori_extension_install_boolean (extension, "always-load", TRUE); - g_signal_connect (extension, "activate", - G_CALLBACK (formhistory_activate_cb), NULL); - g_signal_connect (extension, "open-preferences", - G_CALLBACK (formhistory_preferences_cb), NULL); - } - - return extension; -} diff --git a/extensions/formhistory/formhistory-frontend.h b/extensions/formhistory/formhistory-frontend.h new file mode 100644 index 00000000..764e89e6 --- /dev/null +++ b/extensions/formhistory/formhistory-frontend.h @@ -0,0 +1,63 @@ +/* + Copyright (C) 2009-2012 Alexander Butenko + Copyright (C) 2009-2012 Christian Dywan + + 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. +*/ + +#ifndef __FORMHISTORY_FRONTEND_H__ +#define __FORMHISTORY_FRONTEND_H__ +#include +#include + +#include "config.h" +#if HAVE_UNISTD_H + #include +#endif + +#if WEBKIT_CHECK_VERSION (1, 3, 1) + #define FORMHISTORY_USE_GDOM 1 +#else + #define FORMHISTORY_USE_JS 1 +#endif + +typedef struct +{ + sqlite3* db; + #if FORMHISTORY_USE_GDOM + WebKitDOMElement* element; + int completion_timeout; + GtkTreeModel* completion_model; + GtkWidget* treeview; + GtkWidget* popup; + GtkWidget* root; + gchar* oldkeyword; + guint selection_index; + #else + gchar* jsforms; + #endif +} FormHistoryPriv; + +FormHistoryPriv* +formhistory_private_new (); + +void +formhistory_private_destroy (FormHistoryPriv *priv); + +gboolean +formhistory_construct_popup_gui (FormHistoryPriv* priv); + +void +formhistory_setup_suggestions (WebKitWebView* web_view, + JSContextRef js_context, + MidoriExtension* extension); + +void +formhistory_suggestions_hide_cb (WebKitDOMElement* element, + WebKitDOMEvent* dom_event, + FormHistoryPriv* priv); + +#endif diff --git a/extensions/formhistory/formhistory-gdom-frontend.c b/extensions/formhistory/formhistory-gdom-frontend.c new file mode 100644 index 00000000..230e1a94 --- /dev/null +++ b/extensions/formhistory/formhistory-gdom-frontend.c @@ -0,0 +1,456 @@ +/* + Copyright (C) 2009-2012 Alexander Butenko + Copyright (C) 2009-2012 Christian Dywan + + 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. +*/ +#include "formhistory-frontend.h" +#ifdef FORMHISTORY_USE_GDOM +#define COMPLETION_DELAY 200 + +FormHistoryPriv* +formhistory_private_new () +{ + FormHistoryPriv* priv; + + priv = g_slice_new (FormHistoryPriv); + priv->oldkeyword = g_strdup (""); + priv->selection_index = -1; + return priv; +} + +void +formhistory_suggestions_hide_cb (WebKitDOMElement* element, + WebKitDOMEvent* dom_event, + FormHistoryPriv* priv) +{ + if (gtk_widget_get_visible (priv->popup)) + gtk_widget_hide (priv->popup); + priv->selection_index = -1; +} + +static void +formhistory_suggestion_set (GtkTreePath* path, + FormHistoryPriv* priv) +{ + GtkTreeIter iter; + gchar* value; + + if (!gtk_tree_model_get_iter (priv->completion_model, &iter, path)) + return; + + gtk_tree_model_get (priv->completion_model, &iter, 0, &value, -1); + g_object_set (priv->element, "value", value, NULL); + g_free (value); +} + +static gboolean +formhistory_suggestion_selected_cb (GtkWidget* treeview, + GdkEventButton* event, + FormHistoryPriv* priv) + +{ + GtkTreePath* path; + + if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview), + event->x, event->y, &path, NULL, NULL, NULL)) + { + formhistory_suggestion_set (path, priv); + formhistory_suggestions_hide_cb (NULL, NULL, priv); + gtk_tree_path_free (path); + return TRUE; + } + return FALSE; +} + +static void +get_absolute_offset_for_element (WebKitDOMElement* element, + glong* x, + glong* y, + gboolean mainframe) +{ + WebKitDOMElement* offset_parent; + WebKitDOMDocument* element_document; + gint offset_top = 0, offset_left = 0; + gboolean ismainframe = FALSE; + WebKitDOMNodeList* frames; + WebKitDOMDocument* fdoc; + int i; + + frames = g_object_get_data (G_OBJECT (element), "framelist"); + element_document = g_object_get_data (G_OBJECT (element), "doc"); + g_object_get (element, "offset-left", &offset_left, + "offset-top", &offset_top, + "offset-parent", &offset_parent, + NULL); + *x += offset_left; + *y += offset_top; + + /* To avoid deadlock check only first element of the mainframe parent */ + if (mainframe == TRUE) + return; + + if (offset_parent) + goto finish; + + /* Element havent returned any parents. Thats mean or there is no parents or we are inside the frame + Loop over all frames we have to find frame == element_document which is a root for our element + and get its offsets */ + for (i = 0; i < webkit_dom_node_list_get_length (frames); i++) + { + WebKitDOMNode *frame = webkit_dom_node_list_item (frames, i); + + if (WEBKIT_DOM_IS_HTML_IFRAME_ELEMENT (frame)) + fdoc = webkit_dom_html_iframe_element_get_content_document (WEBKIT_DOM_HTML_IFRAME_ELEMENT (frame)); + else + fdoc = webkit_dom_html_frame_element_get_content_document (WEBKIT_DOM_HTML_FRAME_ELEMENT (frame)); + + if (fdoc == element_document) + { + offset_parent = WEBKIT_DOM_ELEMENT (frame); + ismainframe = TRUE; + /* Add extra 4px to ~cover size of borders */ + *y += 4; + break; + } + } + if (!offset_parent) + return; +finish: + /* Copy set properties to parents as they dont have them set */ + /* FIXME: Seems we need to drop them afterwards to save some memory? */ + g_object_set_data (G_OBJECT (offset_parent), "doc", element_document); + g_object_set_data (G_OBJECT (offset_parent), "framelist", frames); + get_absolute_offset_for_element (offset_parent, x, y, ismainframe); +} + +static void +formhistory_reposition_popup (FormHistoryPriv* priv, + GtkWidget* widget) +{ + GdkWindow* window; + gint rx, ry; + gint wx, wy; + glong x = 0, y = 0; + glong height; + + /* Position of a root window */ + window = gtk_widget_get_window (widget); + gdk_window_get_position (window, &rx, &ry); + + /* Postion of webview in root window */ + window = gtk_widget_get_window (priv->root); + gdk_window_get_position (window, &wx, &wy); + + /* Position of editbox on the webview */ + get_absolute_offset_for_element (priv->element, &x, &y, FALSE); + /* Add height as menu should start under editbox, now on top of it */ + g_object_get (priv->element, "client-height", &height, NULL); + y += height + 1; + + gtk_window_move (GTK_WINDOW (priv->popup), rx + wx + x, ry +wy + y); + gtk_tree_view_columns_autosize (GTK_TREE_VIEW (priv->treeview)); + /* FIXME: Adjust size according to treeview width and some reasonable height */ + gtk_window_resize (GTK_WINDOW (priv->popup), 50, 80); +} + +static void +formhistory_suggestions_show (FormHistoryPriv* priv) +{ + GtkListStore* store; + static sqlite3_stmt* stmt; + const gchar* value; + const gchar* name; + const char* sqlcmd; + gint result; + + g_source_remove (priv->completion_timeout); + + g_object_get (priv->element, + "name", &name, + "value", &value, + NULL); + katze_assign (priv->oldkeyword, g_strdup (value)); + if (!priv->popup) + formhistory_construct_popup_gui (priv); + + if (!stmt) + { + if (!priv->db) + return; + + sqlcmd = "SELECT DISTINCT value FROM forms WHERE field = ?1 and value like ?2"; + sqlite3_prepare_v2 (priv->db, sqlcmd, strlen (sqlcmd) + 1, &stmt, NULL); + } + + gchar* likedvalue = g_strdup_printf ("%s%%", value); + sqlite3_bind_text (stmt, 1, name, -1, NULL); + sqlite3_bind_text (stmt, 2, likedvalue, -1, g_free); + result = sqlite3_step (stmt); + + if (result != SQLITE_ROW) + { + if (result == SQLITE_ERROR) + g_print (_("Failed to select suggestions\n")); + sqlite3_reset (stmt); + sqlite3_clear_bindings (stmt); + formhistory_suggestions_hide_cb (NULL, NULL, priv); + return; + } + + store = GTK_LIST_STORE (priv->completion_model); + gtk_list_store_clear (store); + int pos = 0; + + while (result == SQLITE_ROW) + { + pos++; + const unsigned char* text = sqlite3_column_text (stmt, 0); + gtk_list_store_insert_with_values (store, NULL, pos, 0, text, -1); + result = sqlite3_step (stmt); + } + sqlite3_reset (stmt); + sqlite3_clear_bindings (stmt); + gtk_widget_grab_focus (priv->treeview); + + if (gtk_widget_get_visible (priv->popup)) + return; + + GtkWidget* toplevel = gtk_widget_get_toplevel (GTK_WIDGET (priv->root)); + gtk_window_set_screen (GTK_WINDOW (priv->popup), + gtk_widget_get_screen (GTK_WIDGET (priv->root))); + /* FIXME: If Midori window is small, popup doesn't show up */ + gtk_window_set_transient_for (GTK_WINDOW (priv->popup), GTK_WINDOW (toplevel)); + formhistory_reposition_popup (priv, toplevel); + gtk_widget_show_all (priv->popup); + gtk_widget_grab_focus (priv->treeview); +} + +static void +formhistory_editbox_key_pressed_cb (WebKitDOMElement* element, + WebKitDOMEvent* dom_event, + FormHistoryPriv* priv) +{ + glong key; + GtkTreePath* path; + const gchar* keyword; + + /* FIXME: Priv is still set after module is disabled */ + if (!priv) + return; + + if (priv->completion_timeout > 0) + g_source_remove (priv->completion_timeout); + + g_object_get (element, "value", &keyword, NULL); + priv->element = element; + + key = webkit_dom_ui_event_get_key_code (WEBKIT_DOM_UI_EVENT (dom_event)); + + /* Ignore some control chars */ + if (key < 20 && key != 8) + return; + + gint matches = gtk_tree_model_iter_n_children (priv->completion_model, NULL); + + switch (key) + { + /* ESC key*/ + case 27: + case 35: + case 36: + /* Left key*/ + case 37: + /* Right key*/ + case 39: + if (key == 27) + g_object_set (element, "value", priv->oldkeyword, NULL); + formhistory_suggestions_hide_cb (element, dom_event, priv); + return; + break; + /*FIXME: Del to delete entry */ + /* Up key */ + case 38: + /* Down key */ + case 40: + if (gtk_widget_get_visible (priv->popup)) + { + if (key == 38) + { + if (priv->selection_index == -1) + priv->selection_index = matches - 1; + else + priv->selection_index = MAX (priv->selection_index - 1, 1); + } + else + { + priv->selection_index = MIN (priv->selection_index + 1, matches -1); + } + + path = gtk_tree_path_new_from_indices (priv->selection_index, -1); + gtk_tree_view_set_cursor (GTK_TREE_VIEW (priv->treeview), path, NULL, FALSE); + formhistory_suggestion_set (path, priv); + gtk_tree_path_free (path); + } + else + formhistory_suggestions_show (priv); + return; + break; + /* PgUp, PgDn, Ins */ + case 33: + case 34: + case 45: + break; + } + + if (!(keyword && *keyword && *keyword != ' ')) + { + formhistory_suggestions_hide_cb (element, dom_event, priv); + return; + } + + /* If the same keyword is submitted there's no need to regenerate suggestions */ + if (gtk_widget_get_visible (priv->popup) && + !g_strcmp0 (keyword, priv->oldkeyword)) + return; + priv->completion_timeout = g_timeout_add (COMPLETION_DELAY, + (GSourceFunc)formhistory_suggestions_show, priv); +} + +static void +formhistory_DOMContentLoaded_cb (WebKitDOMElement* window, + WebKitDOMEvent* dom_event, + FormHistoryPriv* priv) +{ + int i; + WebKitDOMDocument* doc; + WebKitDOMNodeList* inputs; + WebKitDOMNodeList* frames; + + if (WEBKIT_DOM_IS_DOCUMENT (window)) + doc = WEBKIT_DOM_DOCUMENT (window); + else + doc = webkit_dom_dom_window_get_document (WEBKIT_DOM_DOM_WINDOW (window)); + inputs = webkit_dom_document_query_selector_all (doc, "input[type='text']", NULL); + frames = g_object_get_data (G_OBJECT (window), "framelist"); + + for (i = 0; i < webkit_dom_node_list_get_length (inputs); i++) + { + const gchar* autocomplete; + WebKitDOMNode* element = webkit_dom_node_list_item (inputs, i); + g_object_get (element, "autocomplete", &autocomplete, NULL); + /* Dont bind if input is not text or autocomplete is disabled */ + if (!g_strcmp0 (autocomplete, "off")) + continue; + + g_object_set_data (G_OBJECT (element), "doc", doc); + g_object_set_data (G_OBJECT (element), "framelist", frames); + /* Add dblclick? */ + webkit_dom_event_target_add_event_listener ( + WEBKIT_DOM_EVENT_TARGET (element), "keyup", + G_CALLBACK (formhistory_editbox_key_pressed_cb), false, + priv); + webkit_dom_event_target_add_event_listener ( + WEBKIT_DOM_EVENT_TARGET (element), "blur", + G_CALLBACK (formhistory_suggestions_hide_cb), false, + priv); + } +} + +void +formhistory_setup_suggestions (WebKitWebView* web_view, + JSContextRef js_context, + MidoriExtension* extension) +{ + WebKitDOMDocument* doc; + WebKitDOMNodeList* frames; + int i; + + FormHistoryPriv* priv = g_object_get_data (G_OBJECT (extension), "priv"); + priv->root = (GtkWidget*)web_view; + doc = webkit_web_view_get_dom_document (web_view); + frames = webkit_dom_document_query_selector_all (doc, "iframe, frame", NULL); + g_object_set_data (G_OBJECT (doc), "framelist", frames); + /* Connect to DOMContentLoaded of the main frame */ + webkit_dom_event_target_add_event_listener( + WEBKIT_DOM_EVENT_TARGET (doc), "DOMContentLoaded", + G_CALLBACK (formhistory_DOMContentLoaded_cb), false, + priv); + + /* Connect to DOMContentLoaded of frames */ + for (i = 0; i < webkit_dom_node_list_get_length (frames); i++) + { + WebKitDOMDOMWindow* framewin; + + WebKitDOMNode* frame = webkit_dom_node_list_item (frames, i); + if (WEBKIT_DOM_IS_HTML_IFRAME_ELEMENT (frame)) + framewin = webkit_dom_html_iframe_element_get_content_window (WEBKIT_DOM_HTML_IFRAME_ELEMENT (frame)); + else + framewin = webkit_dom_html_frame_element_get_content_window (WEBKIT_DOM_HTML_FRAME_ELEMENT (frame)); + g_object_set_data (G_OBJECT (framewin), "framelist", frames); + webkit_dom_event_target_add_event_listener ( + WEBKIT_DOM_EVENT_TARGET (framewin), "DOMContentLoaded", + G_CALLBACK (formhistory_DOMContentLoaded_cb), false, + priv); + } +} + +void +formhistory_private_destroy (FormHistoryPriv *priv) +{ + if (priv->db) + { + sqlite3_close (priv->db); + priv->db = NULL; + } + if (priv->oldkeyword) + g_free (priv->oldkeyword); + gtk_widget_destroy (priv->popup); + priv->popup = NULL; + g_slice_free (FormHistoryPriv, priv); + priv = NULL; +} + +gboolean +formhistory_construct_popup_gui (FormHistoryPriv* priv) +{ + GtkTreeModel* model = NULL; + GtkWidget* popup; + GtkWidget* popup_frame; + GtkWidget* scrolled; + GtkWidget* treeview; + GtkCellRenderer* renderer; + GtkTreeViewColumn* column; + + model = (GtkTreeModel*) gtk_list_store_new (1, G_TYPE_STRING); + priv->completion_model = model; + popup = gtk_window_new (GTK_WINDOW_POPUP); + gtk_window_set_type_hint (GTK_WINDOW (popup), GDK_WINDOW_TYPE_HINT_COMBO); + popup_frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (popup_frame), GTK_SHADOW_ETCHED_IN); + gtk_container_add (GTK_CONTAINER (popup), popup_frame); + 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_frame), scrolled); + treeview = gtk_tree_view_new_with_model (model); + priv->treeview = treeview; + 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); + gtk_widget_set_size_request (gtk_scrolled_window_get_vscrollbar ( + GTK_SCROLLED_WINDOW (scrolled)), -1, 0); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("suggestions", renderer, "text", 0, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); + priv->popup = popup; + + g_signal_connect (treeview, "button-press-event", + G_CALLBACK (formhistory_suggestion_selected_cb), priv); + return TRUE; +} +#endif diff --git a/extensions/formhistory/formhistory-js-frontend.c b/extensions/formhistory/formhistory-js-frontend.c new file mode 100644 index 00000000..1943610a --- /dev/null +++ b/extensions/formhistory/formhistory-js-frontend.c @@ -0,0 +1,149 @@ +/* + Copyright (C) 2009-2012 Alexander Butenko + Copyright (C) 2009-2012 Christian Dywan + + 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. +*/ +#include "formhistory-frontend.h" +#ifdef FORMHISTORY_USE_JS + +FormHistoryPriv* +formhistory_private_new () +{ + FormHistoryPriv* priv; + + priv = g_slice_new (FormHistoryPriv); + return priv; +} + +gboolean +formhistory_construct_popup_gui (FormHistoryPriv* priv) +{ + gchar* autosuggest; + gchar* style; + guint i; + gchar* file; + + file = sokoke_find_data_filename ("autosuggestcontrol.js", TRUE); + if (!g_file_get_contents (file, &autosuggest, NULL, NULL)) + { + g_free (file); + return FALSE; + } + g_strchomp (autosuggest); + + katze_assign (file, sokoke_find_data_filename ("autosuggestcontrol.css", TRUE)); + if (!g_file_get_contents (file, &style, NULL, NULL)) + { + g_free (file); + return FALSE; + } + g_strchomp (style); + g_free (file); + + i = 0; + while (style[i]) + { + if (style[i] == '\n') + style[i] = ' '; + i++; + } + + priv->jsforms = g_strdup_printf ( + "%s" + "window.addEventListener ('DOMContentLoaded'," + "function () {" + " if (document.getElementById('formhistory'))" + " return;" + " if (!initSuggestions ())" + " return;" + " var mystyle = document.createElement('style');" + " mystyle.setAttribute('type', 'text/css');" + " mystyle.setAttribute('id', 'formhistory');" + " mystyle.appendChild(document.createTextNode('%s'));" + " var head = document.getElementsByTagName('head')[0];" + " if (head) head.appendChild(mystyle);" + "}, true);", + autosuggest, + style); + g_strstrip (priv->jsforms); + g_free (style); + g_free (autosuggest); + return TRUE; +} + +void +formhistory_setup_suggestions (WebKitWebView* web_view, + JSContextRef js_context, + MidoriExtension* extension) +{ + GString* suggestions; + FormHistoryPriv* priv; + static sqlite3_stmt* stmt; + const char* sqlcmd; + gint result, pos; + + priv = g_object_get_data (G_OBJECT (extension), "priv"); + if (!priv->db) + return; + + if (!stmt) + { + sqlcmd = "SELECT DISTINCT group_concat(value,'\",\"'), field FROM forms \ + GROUP BY field ORDER BY field"; + sqlite3_prepare_v2 (priv->db, sqlcmd, strlen (sqlcmd) + 1, &stmt, NULL); + } + result = sqlite3_step (stmt); + if (result != SQLITE_ROW) + { + if (result == SQLITE_ERROR) + g_print (_("Failed to select suggestions\n")); + sqlite3_reset (stmt); + return; + } + suggestions = g_string_new ( + "function FormSuggestions(eid) { " + "arr = new Array();"); + + while (result == SQLITE_ROW) + { + pos++; + const unsigned char* value = sqlite3_column_text (stmt, 0); + const unsigned char* key = sqlite3_column_text (stmt, 1); + if (value) + { + g_string_append_printf (suggestions, " arr[\"%s\"] = [\"%s\"]; ", + (gchar*)key, (gchar*)value); + } + result = sqlite3_step (stmt); + } + g_string_append (suggestions, "this.suggestions = arr[eid]; }"); + g_string_append (suggestions, priv->jsforms); + sokoke_js_script_eval (js_context, suggestions->str, NULL); + g_string_free (suggestions, TRUE); +} + +void +formhistory_suggestions_hide_cb (WebKitDOMElement* element, + WebKitDOMEvent* dom_event, + FormHistoryPriv* priv) +{ + /* Unused in JS frontend */ + return; +} + +void +formhistory_private_destroy (FormHistoryPriv *priv) +{ + if (priv->db) + { + sqlite3_close (priv->db); + priv->db = NULL; + } + katze_assign (priv->jsforms, NULL); + g_slice_free (FormHistoryPriv, priv); +} +#endif diff --git a/extensions/formhistory/formhistory.c b/extensions/formhistory/formhistory.c new file mode 100644 index 00000000..b1f71cbe --- /dev/null +++ b/extensions/formhistory/formhistory.c @@ -0,0 +1,421 @@ +/* + Copyright (C) 2009-2012 Alexander Butenko + Copyright (C) 2009-2012 Christian Dywan + + 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. +*/ +#define MAXCHARS 60 +#define MINCHARS 2 +#include "formhistory-frontend.h" + +static void +formhistory_toggle_state_cb (GtkAction* action, + MidoriBrowser* browser); + +static void +formhistory_update_database (gpointer db, + const gchar* key, + const gchar* value) +{ + gchar* sqlcmd; + gchar* errmsg; + gint success; + guint length; + + if (!(value && *value)) + return; + length = strlen (value); + if (length > MAXCHARS || length < MINCHARS) + return; + + sqlcmd = sqlite3_mprintf ("INSERT INTO forms VALUES" + "('%q', '%q', '%q')", + NULL, key, value); + success = sqlite3_exec (db, sqlcmd, NULL, NULL, &errmsg); + sqlite3_free (sqlcmd); + if (success != SQLITE_OK) + { + g_printerr (_("Failed to add form value: %s\n"), errmsg); + g_free (errmsg); + return; + } +} + +static gboolean +formhistory_navigation_decision_cb (WebKitWebView* web_view, + WebKitWebFrame* web_frame, + WebKitNetworkRequest* request, + WebKitWebNavigationAction* action, + WebKitWebPolicyDecision* decision, + MidoriExtension* extension) +{ + /* The script returns form data in the form "field_name|,|value|,|field_type". + We are handling only input fields with 'text' or 'password' type. + The field separator is "|||" */ + const gchar* script = "function dumpForm (inputs) {" + " var out = '';" + " for (i=0;idb, parts[0], parts[1]); + } + g_strfreev (parts); + i++; + } + g_strfreev (inputs); + g_free (value); + } + return FALSE; +} + +static void +formhistory_window_object_cleared_cb (WebKitWebView* web_view, + WebKitWebFrame* web_frame, + JSContextRef js_context, + JSObjectRef js_window, + MidoriExtension* extension) +{ + const gchar* page_uri; + + page_uri = webkit_web_frame_get_uri (web_frame); + if (!page_uri) + return; + + if (!midori_uri_is_http (page_uri) && !g_str_has_prefix (page_uri, "file")) + return; + + formhistory_setup_suggestions (web_view, js_context, extension); +} + +static void +formhistory_deactivate_cb (MidoriExtension* extension, + MidoriBrowser* browser); + +static void +formhistory_add_tab_cb (MidoriBrowser* browser, + MidoriView* view, + MidoriExtension* extension) +{ + g_return_if_fail (MIDORI_IS_VIEW (view)); + g_return_if_fail (MIDORI_IS_EXTENSION (extension)); + GtkWidget* web_view = midori_view_get_web_view (view); + + g_signal_connect (web_view, "window-object-cleared", + G_CALLBACK (formhistory_window_object_cleared_cb), extension); + g_signal_connect (web_view, "navigation-policy-decision-requested", + G_CALLBACK (formhistory_navigation_decision_cb), extension); +} + +static void +formhistory_add_tab_foreach_cb (MidoriView* view, + MidoriExtension* extension) +{ + g_return_if_fail (MIDORI_IS_VIEW (view)); + formhistory_add_tab_cb (NULL, view, extension); +} + +static void +formhistory_app_add_browser_cb (MidoriApp* app, + MidoriBrowser* browser, + MidoriExtension* extension) +{ + g_return_if_fail (MIDORI_IS_APP (app)); + g_return_if_fail (MIDORI_IS_BROWSER (browser)); + g_return_if_fail (MIDORI_IS_EXTENSION (extension)); + + GtkAccelGroup* acg = gtk_accel_group_new (); + GtkActionGroup* action_group = midori_browser_get_action_group (browser); + GtkAction* action = gtk_action_new ("FormHistoryToggleState", + _("Toggle form history state"), + _("Activate or deactivate form history for the current tab."), NULL); + gtk_window_add_accel_group (GTK_WINDOW (browser), acg); + + g_object_set_data (G_OBJECT (browser), "FormHistoryExtension", extension); + + g_signal_connect (action, "activate", + G_CALLBACK (formhistory_toggle_state_cb), browser); + + gtk_action_group_add_action_with_accel (action_group, action, "F"); + gtk_action_set_accel_group (action, acg); + gtk_action_connect_accelerator (action); + + if (midori_extension_get_boolean (extension, "always-load")) + { + midori_browser_foreach (browser, + (GtkCallback)formhistory_add_tab_foreach_cb, extension); + g_signal_connect (browser, "add-tab", + G_CALLBACK (formhistory_add_tab_cb), extension); + } + g_signal_connect (extension, "deactivate", + G_CALLBACK (formhistory_deactivate_cb), browser); +} + +static void +formhistory_deactivate_tab (MidoriView* view, + MidoriExtension* extension) +{ + g_return_if_fail (MIDORI_IS_VIEW (view)); + g_return_if_fail (MIDORI_IS_EXTENSION (extension)); + GtkWidget* web_view = midori_view_get_web_view (view); + + g_signal_handlers_disconnect_by_func ( + web_view, formhistory_window_object_cleared_cb, extension); + g_signal_handlers_disconnect_by_func ( + web_view, formhistory_navigation_decision_cb, extension); +} + +static void +formhistory_deactivate_cb (MidoriExtension* extension, + MidoriBrowser* browser) +{ + MidoriApp* app = midori_extension_get_app (extension); + FormHistoryPriv* priv = g_object_get_data (G_OBJECT (extension), "priv"); + + GtkActionGroup* action_group = midori_browser_get_action_group (browser); + GtkAction* action; + + g_signal_handlers_disconnect_by_func ( + browser, formhistory_add_tab_cb, extension); + g_signal_handlers_disconnect_by_func ( + extension, formhistory_deactivate_cb, browser); + g_signal_handlers_disconnect_by_func ( + app, formhistory_app_add_browser_cb, extension); + midori_browser_foreach (browser, + (GtkCallback)formhistory_deactivate_tab, extension); + + g_object_set_data (G_OBJECT (browser), "FormHistoryExtension", NULL); + action = gtk_action_group_get_action ( action_group, "FormHistoryToggleState"); + if (action != NULL) + { + gtk_action_group_remove_action (action_group, action); + g_object_unref (action); + } + + formhistory_private_destroy (priv); +} + +static void +formhistory_activate_cb (MidoriExtension* extension, + MidoriApp* app) +{ + const gchar* config_dir; + gchar* filename; + sqlite3* db; + char* errmsg = NULL, *errmsg2 = NULL; + KatzeArray* browsers; + MidoriBrowser* browser; + FormHistoryPriv* priv; + + priv = formhistory_private_new (); + formhistory_construct_popup_gui (priv); + + config_dir = midori_extension_get_config_dir (extension); + katze_mkdir_with_parents (config_dir, 0700); + filename = g_build_filename (config_dir, "forms.db", NULL); + if (sqlite3_open (filename, &db) != SQLITE_OK) + { + /* If the folder is /, this is a test run, thus no error */ + if (!g_str_equal (midori_extension_get_config_dir (extension), "/")) + g_warning (_("Failed to open database: %s\n"), sqlite3_errmsg (db)); + sqlite3_close (db); + } + g_free (filename); + if ((sqlite3_exec (db, "CREATE TABLE IF NOT EXISTS " + "forms (domain text, field text, value text)", + NULL, NULL, &errmsg) == SQLITE_OK)) + { + priv->db = db; + } + else + { + if (errmsg) + { + g_critical (_("Failed to execute database statement: %s\n"), errmsg); + sqlite3_free (errmsg); + if (errmsg2) + { + g_critical (_("Failed to execute database statement: %s\n"), errmsg2); + sqlite3_free (errmsg2); + } + } + sqlite3_close (db); + } + + g_object_set_data (G_OBJECT (extension), "priv", priv); + browsers = katze_object_get_object (app, "browsers"); + KATZE_ARRAY_FOREACH_ITEM (browser, browsers) + formhistory_app_add_browser_cb (app, browser, extension); + g_signal_connect (app, "add-browser", + G_CALLBACK (formhistory_app_add_browser_cb), extension); + + g_object_unref (browsers); +} + +static void +formhistory_preferences_response_cb (GtkWidget* dialog, + gint response_id, + MidoriExtension* extension) +{ + GtkWidget* checkbox; + gboolean old_state; + gboolean new_state; + MidoriApp* app; + KatzeArray* browsers; + MidoriBrowser* browser; + + if (response_id == GTK_RESPONSE_APPLY) + { + checkbox = g_object_get_data (G_OBJECT (dialog), "always-load-checkbox"); + new_state = !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox)); + old_state = midori_extension_get_boolean (extension, "always-load"); + + if (old_state != new_state) + { + midori_extension_set_boolean (extension, "always-load", new_state); + + app = midori_extension_get_app (extension); + browsers = katze_object_get_object (app, "browsers"); + KATZE_ARRAY_FOREACH_ITEM (browser, browsers) + { + midori_browser_foreach (browser, + (GtkCallback)formhistory_deactivate_tab, extension); + g_signal_handlers_disconnect_by_func ( + browser, formhistory_add_tab_cb, extension); + + if (new_state) + { + midori_browser_foreach (browser, + (GtkCallback)formhistory_add_tab_foreach_cb, extension); + g_signal_connect (browser, "add-tab", + G_CALLBACK (formhistory_add_tab_cb), extension); + } + } + } + } + gtk_widget_destroy (dialog); +} + +static void +formhistory_preferences_cb (MidoriExtension* extension) +{ + GtkWidget* dialog; + GtkWidget* content_area; + GtkWidget* checkbox; + + dialog = gtk_dialog_new (); + + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + + gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_APPLY, GTK_RESPONSE_APPLY); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + checkbox = gtk_check_button_new_with_label (_("only activate form history via hotkey (Ctrl+Shift+F) per tab")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (checkbox), + !midori_extension_get_boolean (extension, "always-load")); + g_object_set_data (G_OBJECT (dialog), "always-load-checkbox", checkbox); + gtk_container_add (GTK_CONTAINER (content_area), checkbox); + + g_signal_connect (dialog, + "response", + G_CALLBACK (formhistory_preferences_response_cb), + extension); + gtk_widget_show_all (dialog); +} + +static void +formhistory_toggle_state_cb (GtkAction* action, + MidoriBrowser* browser) +{ + MidoriView* view = MIDORI_VIEW (midori_browser_get_current_tab (browser)); + MidoriExtension* extension = g_object_get_data (G_OBJECT (browser), "FormHistoryExtension"); + GtkWidget* web_view = midori_view_get_web_view (view); + + if (g_signal_handler_find (web_view, G_SIGNAL_MATCH_FUNC, + g_signal_lookup ("window-object-cleared", MIDORI_TYPE_VIEW), 0, NULL, + formhistory_window_object_cleared_cb, extension)) + { + formhistory_deactivate_tab (view, extension); + } else { + formhistory_add_tab_cb (browser, view, extension); + } +} + + +#if G_ENABLE_DEBUG +/* + + + autosuggest testcase + + +
+

+

+ +
+ + */ +#endif + +MidoriExtension* +extension_init (void) +{ + const gchar* ver; + gchar* desc; + MidoriExtension* extension; + + ver = "2.0" MIDORI_VERSION_SUFFIX; + desc = g_strdup (_("Stores history of entered form data")); + + extension = g_object_new (MIDORI_TYPE_EXTENSION, + "name", _("Form history filler"), + "description", desc, + "version", ver, + "authors", "Alexander V. Butenko ", + NULL); + + g_free (desc); + + midori_extension_install_boolean (extension, "always-load", TRUE); + g_signal_connect (extension, "activate", + G_CALLBACK (formhistory_activate_cb), NULL); + g_signal_connect (extension, "open-preferences", + G_CALLBACK (formhistory_preferences_cb), NULL); + + return extension; +}