From d445745787919695bf57b1d557c2d02642a05a5a Mon Sep 17 00:00:00 2001 From: Dale Whittaker Date: Tue, 7 Oct 2008 02:19:33 +0200 Subject: [PATCH] Implement navigation history backed by sqlite The implementation relies on KatzeArray for the most part and only does the storage with sqlite behind the scenes. The change includes a working History panel. Changes to the database are committed in realtime. --- configure.in | 6 + katze/katze-item.c | 105 ++++++++- katze/katze-item.h | 16 ++ katze/katze-list.c | 43 +++- katze/katze-list.h | 3 + midori/main.c | 309 +++++++++++++++++++++++++ midori/midori-app.c | 25 ++ midori/midori-browser.c | 492 +++++++++++++++++++++++++++++++++++++++- midori/wscript_build | 2 +- wscript | 1 + 10 files changed, 982 insertions(+), 20 deletions(-) diff --git a/configure.in b/configure.in index 777efd08..fa102248 100644 --- a/configure.in +++ b/configure.in @@ -83,6 +83,12 @@ AC_SUBST(LIBXML_CFLAGS) AC_SUBST(LIBXML_LIBS) AC_DEFINE_UNQUOTED(HAVE_LIBXML,$have_libxml, [Whether LibXML is available]) +# Check for sqlite 3 +PKG_CHECK_MODULES([SQLITE3], [sqlite3 >= 3.0], have_sqlite3=1, have_sqlite3=0) +AC_SUBST([SQLITE3_CFLAGS]) +AC_SUBST([SQLITE3_LIBS]) +AC_DEFINE_UNQUOTED(HAVE_SQLITE3,$have_sqlite3, [Whether sqlite3 is available]) + # i18n GETTEXT_PACKAGE=midori AC_SUBST(GETTEXT_PACKAGE) diff --git a/katze/katze-item.c b/katze/katze-item.c index f71d8d05..46e0b8a0 100644 --- a/katze/katze-item.c +++ b/katze/katze-item.c @@ -34,7 +34,9 @@ enum PROP_TEXT, PROP_URI, PROP_ICON, - PROP_TOKEN + PROP_TOKEN, + PROP_ADDED, + PROP_VISITS }; static void @@ -109,6 +111,27 @@ katze_item_class_init (KatzeItemClass* class) _("The token of the item"), NULL, flags)); + + g_object_class_install_property (gobject_class, + PROP_ADDED, + g_param_spec_string ( + "added", + _("Added"), + _("When the item was added"), + NULL, + flags)); + + g_object_class_install_property (gobject_class, + PROP_VISITS, + g_param_spec_int ( + "visits", + _("Visits"), + _("The number of visits of the item"), + G_MININT, + G_MAXINT, + 0, + flags)); + } @@ -129,6 +152,7 @@ katze_item_finalize (GObject* object) g_free (item->uri); g_free (item->icon); g_free (item->token); + g_free (item->added); G_OBJECT_CLASS (katze_item_parent_class)->finalize (object); } @@ -158,6 +182,12 @@ katze_item_set_property (GObject* object, case PROP_TOKEN: item->token = g_value_dup_string (value); break; + case PROP_ADDED: + item->added = g_value_dup_string (value); + break; + case PROP_VISITS: + item->visits = g_value_get_int (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -189,6 +219,12 @@ katze_item_get_property (GObject* object, case PROP_TOKEN: g_value_set_string (value, item->token); break; + case PROP_ADDED: + g_value_set_string (value, item->added); + break; + case PROP_VISITS: + g_value_set_int (value, item->visits); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -375,6 +411,72 @@ katze_item_set_token (KatzeItem* item, g_object_notify (G_OBJECT (item), "token"); } +/** + * katze_item_get_added: + * @item: a #KatzeItem + * + * Determines when @item was added. + * + * Return value: a timestamp + **/ +const gchar* +katze_item_get_added (KatzeItem* item) +{ + g_return_val_if_fail (KATZE_IS_ITEM (item), NULL); + + return item->added; +} + +/** + * katze_item_set_added: + * @item: a #KatzeItem + * @added: a timestamp + * + * Sets when @item was added. + **/ +void +katze_item_set_added (KatzeItem* item, + const gchar* added) +{ + g_return_if_fail (KATZE_IS_ITEM (item)); + + katze_assign (item->added, g_strdup (added)); + g_object_notify (G_OBJECT (item), "added"); +} + +/** + * katze_item_get_visits: + * @item: a #KatzeItem + * + * Retrieves the number of visits of @item. + * + * Return value: the number of visits + **/ +gint +katze_item_get_visits (KatzeItem* item) +{ + g_return_val_if_fail (KATZE_IS_ITEM (item), -1); + + return item->visits; +} + +/** + * katze_item_set_visits: + * @item: a #KatzeItem + * @visits: an integer + * + * Sets the number of visits of @item. + **/ +void +katze_item_set_visits (KatzeItem* item, + gint visits) +{ + g_return_if_fail (KATZE_IS_ITEM (item)); + + item->visits = visits; + g_object_notify (G_OBJECT (item), "visits"); +} + /** * katze_item_get_parent: * @item: a #KatzeItem @@ -412,3 +514,4 @@ katze_item_set_parent (KatzeItem* item, katze_object_assign (item->parent, parent); /* g_object_notify (G_OBJECT (item), "parent"); */ } + diff --git a/katze/katze-item.h b/katze/katze-item.h index 7a921c97..8014b57a 100644 --- a/katze/katze-item.h +++ b/katze/katze-item.h @@ -41,6 +41,8 @@ struct _KatzeItem gchar* uri; gchar* icon; gchar* token; + gchar* added; + gint visits; KatzeItem* parent; }; @@ -91,6 +93,20 @@ void katze_item_set_token (KatzeItem* item, const gchar* token); +const gchar* +katze_item_get_added (KatzeItem* item); + +void +katze_item_set_added (KatzeItem* item, + const gchar* added); + +gint +katze_item_get_visits (KatzeItem* item); + +void +katze_item_set_visits (KatzeItem* item, + gint visits); + gpointer katze_item_get_parent (KatzeItem* item); diff --git a/katze/katze-list.c b/katze/katze-list.c index 15bb0e1e..cb0fe80d 100644 --- a/katze/katze-list.c +++ b/katze/katze-list.c @@ -29,6 +29,7 @@ G_DEFINE_TYPE (KatzeList, katze_list, KATZE_TYPE_ITEM) enum { ADD_ITEM, REMOVE_ITEM, + CLEAR, LAST_SIGNAL }; @@ -52,6 +53,23 @@ _katze_list_remove_item (KatzeList* list, list->items = g_list_remove (list->items, item); } +static void +_katze_list_clear (KatzeList* list) +{ + guint n; + guint i; + GObject* item; + + n = g_list_length (list->items); + for (i = 0; i < n; i++) + { + if ((item = g_list_nth_data (list->items, i))) + katze_list_remove_item (list, item); + } + g_list_free (list->items); + list->items = NULL; +} + static void katze_list_class_init (KatzeListClass* class) { @@ -79,11 +97,23 @@ katze_list_class_init (KatzeListClass* class) G_TYPE_NONE, 1, G_TYPE_POINTER); + signals[CLEAR] = g_signal_new ( + "clear", + G_TYPE_FROM_CLASS (class), + (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), + G_STRUCT_OFFSET (KatzeListClass, clear), + 0, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gobject_class = G_OBJECT_CLASS (class); gobject_class->finalize = katze_list_finalize; class->add_item = _katze_list_add_item; class->remove_item = _katze_list_remove_item; + class->clear = _katze_list_clear; } static void @@ -227,18 +257,7 @@ katze_list_get_length (KatzeList* list) void katze_list_clear (KatzeList* list) { - guint n; - guint i; - GObject* item; - g_return_if_fail (KATZE_IS_LIST (list)); - n = g_list_length (list->items); - for (i = 0; i < n; i++) - { - if ((item = g_list_nth_data (list->items, i))) - katze_list_remove_item (list, item); - } - g_list_free (list->items); - list->items = NULL; + g_signal_emit (list, signals[CLEAR], 0); } diff --git a/katze/katze-list.h b/katze/katze-list.h index 0e2570db..4d5d9d93 100644 --- a/katze/katze-list.h +++ b/katze/katze-list.h @@ -50,6 +50,9 @@ struct _KatzeListClass void (*remove_item) (KatzeList* list, gpointer item); + void + (*clear) (KatzeList* list); + }; GType diff --git a/midori/main.c b/midori/main.c index dfd6f602..bb6e4ed3 100644 --- a/midori/main.c +++ b/midori/main.c @@ -1,5 +1,6 @@ /* Copyright (C) 2007-2008 Christian Dywan + Copyright (C) 2008 Dale Whittaker This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -27,11 +28,21 @@ #include #include #include +#include #if ENABLE_NLS #include #endif +#define MIDORI_HISTORY_ERROR g_quark_from_string("MIDORI_HISTORY_ERROR") + +typedef enum +{ + MIDORI_HISTORY_ERROR_DB_OPEN, /* Error opening the database file */ + MIDORI_HISTORY_ERROR_EXEC_SQL, /* Error executing SQL statement */ + +} MidoriHistoryError; + static void stock_items_init (void) { @@ -564,6 +575,283 @@ katze_array_from_file (KatzeArray* array, return TRUE; } +/* Open database 'dbname' */ +static sqlite3* +db_open (const char* dbname, + GError** error) +{ + sqlite3* db; + + if (sqlite3_open (dbname, &db)) + { + if (error) + { + *error = g_error_new (MIDORI_HISTORY_ERROR, + MIDORI_HISTORY_ERROR_DB_OPEN, + _("Error opening database: %s\n"), + sqlite3_errmsg (db)); + } + sqlite3_close (db); + return NULL; + } + return (db); +} + +/* Close database 'db' */ +static void +db_close (sqlite3* db) +{ + sqlite3_close (db); +} + +/* Execute an SQL statement and run 'callback' on the result data */ +static gboolean +db_exec_callback (sqlite3* db, + const char* sqlcmd, + int (*callback)(void*, int, char**, char**), + void* cbarg, + GError** error) +{ + char* errmsg; + + if (sqlite3_exec (db, sqlcmd, callback, cbarg, &errmsg) != SQLITE_OK) + { + if (error) + { + *error = g_error_new (MIDORI_HISTORY_ERROR, + MIDORI_HISTORY_ERROR_EXEC_SQL, + _("Error opening database: %s\n"), + errmsg); + } + sqlite3_free (errmsg); + return FALSE; + } + return TRUE; +} + +/* Execute a SQL statement */ +static gboolean +db_exec (sqlite3* db, + const char* sqlcmd, + GError** error) +{ + return (db_exec_callback (db, sqlcmd, NULL, NULL, error)); +} + +/* sqlite method for retrieving the date/ time */ +static int +gettimestr (void* data, + int argc, + char** argv, + char** colname) +{ + KatzeItem* item = KATZE_ITEM (data); + (void) colname; + + g_return_val_if_fail (argc == 1, 1); + + katze_item_set_added (item, argv[0]); + return 0; +} + +static void +midori_history_remove_item_cb (KatzeArray* history, + KatzeItem* item, + sqlite3* db) +{ + gchar* sqlcmd; + gboolean success = TRUE; + GError* error = NULL; + + g_return_if_fail (KATZE_IS_ITEM (item)); + + sqlcmd = g_strdup_printf ("DELETE FROM history WHERE uri = '%s' AND" + " title = '%s' AND date = '%s' AND visits = %d", + katze_item_get_uri (item), + katze_item_get_name (item), + katze_item_get_added (item), + katze_item_get_visits (item)); + success = db_exec (db, sqlcmd, &error); + if (!success) + { + g_printerr (_("Failed to remove history item. %s\n"), error->message); + g_error_free (error); + return ; + } + g_free (sqlcmd); +} + +static void +midori_history_clear_before_cb (KatzeArray* item, + sqlite3* db) +{ + g_signal_handlers_block_by_func (item, midori_history_remove_item_cb, db); +} + +static void +midori_history_clear_cb (KatzeArray* history, + sqlite3* db) +{ + GError* error = NULL; + + g_return_if_fail (KATZE_IS_ARRAY (history)); + + if (!db_exec (db, "DELETE FROM history", &error)) + { + g_printerr (_("Failed to clear history. %s\n"), error->message); + g_error_free (error); + } +} + +static void +midori_history_add_item_cb (KatzeArray* array, + KatzeItem* item, + sqlite3* db) +{ + gchar* sqlcmd; + gboolean success = TRUE; + GError* error = NULL; + + g_return_if_fail (KATZE_IS_ITEM (item)); + + if (KATZE_IS_ARRAY (item)) + { + g_signal_connect_after (item, "add-item", + G_CALLBACK (midori_history_add_item_cb), db); + g_signal_connect (item, "remove-item", + G_CALLBACK (midori_history_remove_item_cb), db); + g_signal_connect (item, "clear", + G_CALLBACK (midori_history_clear_before_cb), db); + return; + } + + /* New item, set added to the current date/ time */ + if (!katze_item_get_added (item)) + { + if (!db_exec_callback (db, "SELECT datetime('now')", + gettimestr, item, &error)) + { + g_printerr (_("Failed to add history item. %s\n"), error->message); + g_error_free (error); + return; + } + } + sqlcmd = g_strdup_printf ("INSERT INTO history VALUES" + "('%s', '%s', '%s', %d)", + katze_item_get_uri (item), + katze_item_get_name (item), + katze_item_get_added (item), + katze_item_get_visits (item)); + success = db_exec (db, sqlcmd, &error); + g_free (sqlcmd); + if (!success) + { + g_printerr (_("Failed to add history item. %s\n"), error->message); + g_error_free (error); + return ; + } +} + +static int +midori_history_add_items (void* data, + int argc, + char** argv, + char** colname) +{ + KatzeItem* item; + KatzeArray* parent = NULL; + KatzeArray* array = KATZE_ARRAY (data); + gchar* newdate; + gint i, j, n; + gint ncols = 4; + gsize len; + + g_return_val_if_fail (KATZE_IS_ARRAY (array), 1); + + /* Test whether have the right number of columns */ + g_return_val_if_fail (argc % ncols == 0, 1); + + for (i = 0; i <= (argc - ncols); i++) + { + if (argv[i]) + { + if (colname[i] && g_ascii_strcasecmp (colname[i], "uri") == 0 && + colname[i + 1] && g_ascii_strcasecmp (colname[i + 1], "title") == 0 && + colname[i + 2] && g_ascii_strcasecmp (colname[i + 2], "date") == 0 && + colname[i + 3] && g_ascii_strcasecmp (colname[i + 3], "visits") == 0) + { + item = katze_item_new (); + katze_item_set_uri (item, argv[i]); + katze_item_set_name (item, argv[i + 1]); + katze_item_set_added (item, argv[i + 2]); + katze_item_set_visits (item, atoi (argv[i + 3])); + + len = (g_strrstr (argv[i + 2], " ") - argv[i + 2]); + newdate = g_strndup (argv[i + 2], len); + + n = katze_array_get_length (array); + for (j = 0; j < n; j++) + { + parent = katze_array_get_nth_item (array, j); + if (newdate && g_ascii_strcasecmp + (katze_item_get_added (KATZE_ITEM (parent)), newdate) == 0) + break; + } + if (j == n) + { + parent = katze_array_new (KATZE_TYPE_ARRAY); + katze_item_set_added (KATZE_ITEM (parent), newdate); + katze_array_add_item (array, parent); + } + g_free (newdate); + + katze_array_add_item (parent, item); + } + } + } + return 0; +} + +static sqlite3* +midori_history_initialize (KatzeArray* array, + const gchar* filename, + GError** error) +{ + sqlite3* db; + KatzeItem* item; + gint i, n; + + if ((db = db_open (filename, error)) == NULL) + return db; + + if (!db_exec (db, + "CREATE TABLE IF NOT EXISTS " + "history(uri text, title text, date text, visits integer)", + error)) + return NULL; + + if (!db_exec_callback (db, + "SELECT uri, title, date, visits FROM history " + "ORDER BY strftime('%s', date) ASC", + midori_history_add_items, + array, + error)) + return NULL; + + n = katze_array_get_length (array); + for (i = 0; i < n; i++) + { + item = katze_array_get_nth_item (array, i); + g_signal_connect_after (item, "add-item", + G_CALLBACK (midori_history_add_item_cb), db); + g_signal_connect (item, "remove-item", + G_CALLBACK (midori_history_remove_item_cb), db); + g_signal_connect (item, "clear", + G_CALLBACK (midori_history_clear_before_cb), db); + } + return db; +} + static gchar* _simple_xml_element (const gchar* name, const gchar* value) @@ -756,6 +1044,7 @@ main (int argc, gchar* homepage; KatzeArray* search_engines; KatzeArray* bookmarks; + KatzeArray* history; KatzeArray* _session; KatzeArray* trash; MidoriBrowser* browser; @@ -764,6 +1053,7 @@ main (int argc, gchar* uri; KatzeItem* item; gchar* uri_ready; + sqlite3* db; #if ENABLE_NLS bindtextdomain (GETTEXT_PACKAGE, MIDORI_LOCALEDIR); @@ -910,6 +1200,16 @@ main (int argc, g_error_free (error); } g_free (config_file); + config_file = g_build_filename (config_path, "history.db", NULL); + history = katze_array_new (KATZE_TYPE_ARRAY); + error = NULL; + if ((db = midori_history_initialize (history, config_file, &error)) == NULL) + { + g_string_append_printf (error_messages, + _("The history couldn't be loaded. %s\n"), error->message); + g_error_free (error); + } + g_free (config_file); /* In case of errors */ if (error_messages->len) @@ -943,6 +1243,7 @@ main (int argc, g_object_unref (bookmarks); g_object_unref (_session); g_object_unref (trash); + g_object_unref (history); g_string_free (error_messages, TRUE); return 0; } @@ -987,11 +1288,16 @@ main (int argc, g_signal_connect_after (trash, "add-item", G_CALLBACK (midori_web_list_add_item_cb), NULL); + g_signal_connect_after (history, "add-item", + G_CALLBACK (midori_history_add_item_cb), db); + g_signal_connect_after (history, "clear", + G_CALLBACK (midori_history_clear_cb), db); g_object_set (app, "settings", settings, "bookmarks", bookmarks, "trash", trash, "search-engines", search_engines, + "history", history, NULL); browser = g_object_new (MIDORI_TYPE_BROWSER, @@ -999,6 +1305,7 @@ main (int argc, "bookmarks", bookmarks, "trash", trash, "search-engines", search_engines, + "history", history, NULL); midori_app_add_browser (app, browser); gtk_widget_show (GTK_WIDGET (browser)); @@ -1059,6 +1366,8 @@ main (int argc, config_path = g_build_filename (g_get_user_config_dir (), PACKAGE_NAME, NULL); g_mkdir_with_parents (config_path, 0755); + g_object_unref (history); + db_close (db); config_file = g_build_filename (config_path, "search", NULL); error = NULL; if (!search_engines_save_to_file (search_engines, config_file, &error)) diff --git a/midori/midori-app.c b/midori/midori-app.c index d5db66fb..4c77f0c8 100644 --- a/midori/midori-app.c +++ b/midori/midori-app.c @@ -35,6 +35,7 @@ struct _MidoriApp KatzeArray* bookmarks; KatzeArray* trash; KatzeArray* search_engines; + KatzeArray* history; gpointer instance; }; @@ -52,6 +53,7 @@ enum PROP_TRASH, PROP_SEARCH_ENGINES, PROP_BROWSER, + PROP_HISTORY, PROP_BROWSER_COUNT }; @@ -172,6 +174,17 @@ midori_app_class_init (MidoriAppClass* class) _("The current number of browsers"), 0, G_MAXUINT, 0, G_PARAM_READABLE)); + + g_object_class_install_property (gobject_class, + PROP_HISTORY, + g_param_spec_object ( + "history", + _("History"), + _("The list of history items"), + KATZE_TYPE_ARRAY, + G_PARAM_READWRITE)); + + } static GObject* @@ -212,6 +225,7 @@ midori_browser_message_received_cb (UniqueApp* instance, "bookmarks", app->bookmarks, "trash", app->trash, "search-engines", app->search_engines, + "history", app->history, NULL); /* FIXME: Should open the homepage according to settings */ midori_browser_add_uri (browser, "about:blank"); @@ -267,6 +281,7 @@ midori_app_init (MidoriApp* app) app->bookmarks = NULL; app->trash = NULL; app->search_engines = NULL; + app->history = NULL; #if HAVE_UNIQUE display_name = g_strdup (gdk_display_get_name (gdk_display_get_default ())); @@ -301,6 +316,8 @@ midori_app_finalize (GObject* object) g_object_unref (app->trash); if (app->search_engines) g_object_unref (app->search_engines); + if (app->history) + g_object_unref (app->history); if (app->instance) g_object_unref (app->instance); @@ -338,6 +355,10 @@ midori_app_set_property (GObject* object, g_object_ref (app->search_engines); /* FIXME: Propagate search engines to all browsers */ break; + case PROP_HISTORY: + katze_object_assign (app->history, g_value_get_object (value)); + /* FIXME: Propagate history to all browsers */ + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -366,6 +387,9 @@ midori_app_get_property (GObject* object, case PROP_SEARCH_ENGINES: g_value_set_object (value, app->search_engines); break; + case PROP_HISTORY: + g_value_set_object (value, app->history); + break; case PROP_BROWSER: g_value_set_object (value, app->browser); break; @@ -398,6 +422,7 @@ midori_browser_new_window_cb (MidoriBrowser* browser, "bookmarks", app->bookmarks, "trash", app->trash, "search-engines", app->search_engines, + "history", app->history, NULL); midori_browser_add_uri (new_browser, uri); gtk_widget_show (GTK_WIDGET (new_browser)); diff --git a/midori/midori-browser.c b/midori/midori-browser.c index 24426dbc..0608ed57 100644 --- a/midori/midori-browser.c +++ b/midori/midori-browser.c @@ -1,5 +1,6 @@ /* Copyright (C) 2007-2008 Christian Dywan + Copyright (C) 2008 Dale Whittaker This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -47,6 +48,7 @@ struct _MidoriBrowser GtkWidget* menu_tools; GtkWidget* menu_window; GtkWidget* popup_bookmark; + GtkWidget* popup_history; GtkWidget* throbber; GtkWidget* navigationbar; GtkWidget* button_tab_new; @@ -58,6 +60,7 @@ struct _MidoriBrowser GtkWidget* panel; GtkWidget* panel_bookmarks; + GtkWidget* panel_history; GtkWidget* panel_console; GtkWidget* panel_pageholder; GtkWidget* notebook; @@ -77,6 +80,7 @@ struct _MidoriBrowser KatzeArray* proxy_array; KatzeArray* trash; KatzeArray* search_engines; + KatzeArray* history; }; G_DEFINE_TYPE (MidoriBrowser, midori_browser, GTK_TYPE_WINDOW) @@ -94,7 +98,8 @@ enum PROP_SETTINGS, PROP_BOOKMARKS, PROP_TRASH, - PROP_SEARCH_ENGINES + PROP_SEARCH_ENGINES, + PROP_HISTORY }; enum @@ -130,7 +135,9 @@ midori_browser_get_property (GObject* object, GValue* value, GParamSpec* pspec); - +static void +midori_browser_new_history_item (MidoriBrowser* browser, + KatzeItem* item); static GtkAction* _action_by_name (MidoriBrowser* browser, @@ -435,12 +442,24 @@ midori_view_notify_title_cb (GtkWidget* view, const gchar* title; GtkAction* action; gchar* window_title; + KatzeItem* item; uri = midori_view_get_display_uri (MIDORI_VIEW (view)); title = midori_view_get_display_title (MIDORI_VIEW (view)); action = _action_by_name (browser, "Location"); midori_location_action_set_title_for_uri ( MIDORI_LOCATION_ACTION (action), title, uri); + if (midori_view_get_load_status (MIDORI_VIEW (view)) == MIDORI_LOAD_COMMITTED) + { + if (!browser->history) + return; + + item = katze_item_new (); + katze_item_set_uri (item, uri); + katze_item_set_name (item, title); + katze_item_set_visits (item, -1); + midori_browser_new_history_item (browser, item); + } if (view == midori_browser_get_current_tab (browser)) { @@ -1094,6 +1113,23 @@ midori_browser_class_init (MidoriBrowserClass* class) _("The list of search engines to be used for web search"), KATZE_TYPE_ARRAY, G_PARAM_READWRITE)); + + /** + * MidoriBrowser:history: + * + * The list of history items. + * + * This is actually a reference to a history instance, + * so if history should be used it must be initially set. + */ + g_object_class_install_property (gobject_class, + PROP_HISTORY, + g_param_spec_object ( + "history", + _("History"), + _("The list of history items"), + KATZE_TYPE_ARRAY, + G_PARAM_READWRITE)); } static void @@ -1941,6 +1977,98 @@ midori_panel_bookmarks_popup_menu_cb (GtkWidget* widget, } } +static void +midori_panel_history_row_activated_cb (GtkTreeView* treeview, + GtkTreePath* path, + GtkTreeViewColumn* column, + MidoriBrowser* browser) +{ + KatzeItem* item; + GtkTreeModel* model; + GtkTreeIter iter; + const gchar* uri; + + model = gtk_tree_view_get_model (treeview); + if (gtk_tree_model_get_iter (model, &iter, path)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + if (KATZE_IS_ITEM (item)) + { + uri = katze_item_get_uri (item); + _midori_browser_open_uri (browser, uri); + } + g_object_unref (item); + } +} + +static void +_midori_panel_history_popup (GtkWidget* widget, + GdkEventButton* event, + KatzeItem* item, + MidoriBrowser* browser) +{ + gboolean is_history_item = (KATZE_IS_ITEM (item) && !KATZE_IS_ARRAY (item)); + + _action_set_sensitive (browser, "HistoryOpen", is_history_item); + _action_set_sensitive (browser, "HistoryOpenTab", is_history_item); + + sokoke_widget_popup (widget, GTK_MENU (browser->popup_history), + event, SOKOKE_MENU_POSITION_CURSOR); +} + +static gboolean +midori_panel_history_button_release_event_cb (GtkWidget* widget, + GdkEventButton* event, + MidoriBrowser* browser) +{ + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + const gchar* uri; + gint n; + + if (event->button != 2 && event->button != 3) + return FALSE; + + if (sokoke_tree_view_get_selected_iter (GTK_TREE_VIEW (widget), + &model, &iter)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + uri = katze_item_get_uri (item); + if (event->button == 2) + { + if (uri && *uri) + { + n = midori_browser_add_uri (browser, uri); + midori_browser_set_current_page (browser, n); + } + } + else + _midori_panel_history_popup (widget, event, item, browser); + + g_object_unref (item); + return TRUE; + } + return FALSE; +} + +static void +midori_panel_history_popup_menu_cb (GtkWidget* widget, + MidoriBrowser* browser) +{ + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + + if (sokoke_tree_view_get_selected_iter (GTK_TREE_VIEW (widget), + &model, &iter)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + _midori_panel_history_popup (widget, NULL, item, browser); + g_object_unref (item); + } +} + static void _tree_store_insert_folder (GtkTreeStore* treestore, GtkTreeIter* parent, @@ -2131,6 +2259,70 @@ _action_bookmark_add_activate (GtkAction* action, midori_browser_edit_bookmark_dialog_new (browser, NULL); } +static void +midori_browser_history_render_icon_cb (GtkTreeViewColumn* column, + GtkCellRenderer* renderer, + GtkTreeModel* model, + GtkTreeIter* iter, + GtkWidget* treeview) +{ + KatzeItem* item; + GdkPixbuf* pixbuf = NULL; + + gtk_tree_model_get (model, iter, 0, &item, -1); + + if (G_UNLIKELY (!item)) + return; + if (G_UNLIKELY (!katze_item_get_parent (item))) + { + gtk_tree_store_remove (GTK_TREE_STORE (model), iter); + g_object_unref (item); + return; + } + + if (KATZE_IS_ARRAY (item)) + pixbuf = gtk_widget_render_icon (treeview, GTK_STOCK_DIRECTORY, + GTK_ICON_SIZE_MENU, NULL); + else + pixbuf = gtk_widget_render_icon (treeview, GTK_STOCK_FILE, + GTK_ICON_SIZE_MENU, NULL); + + g_object_set (renderer, "pixbuf", pixbuf, NULL); + + if (pixbuf) + g_object_unref (pixbuf); + + g_object_unref (item); +} + +static void +midori_browser_history_render_text_cb (GtkTreeViewColumn* column, + GtkCellRenderer* renderer, + GtkTreeModel* model, + GtkTreeIter* iter, + GtkWidget* treeview) +{ + KatzeItem* item; + + gtk_tree_model_get (model, iter, 0, &item, -1); + + if (G_UNLIKELY (!item)) + return; + if (G_UNLIKELY (!katze_item_get_parent (item))) + { + gtk_tree_store_remove (GTK_TREE_STORE (model), iter); + g_object_unref (item); + return; + } + + if (KATZE_IS_ARRAY (item)) + g_object_set (renderer, "text", katze_item_get_added (item), NULL); + else + g_object_set (renderer, "text", katze_item_get_name (item), NULL); + + g_object_unref (item); +} + static void _action_manage_search_engines_activate (GtkAction* action, MidoriBrowser* browser) @@ -2419,6 +2611,120 @@ _action_bookmark_edit_activate (GtkAction* action, } } +static void +_action_history_open_activate (GtkAction* action, + MidoriBrowser* browser) +{ + GtkTreeView* tree_view; + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + const gchar* uri; + + tree_view = GTK_TREE_VIEW (browser->panel_history); + if (sokoke_tree_view_get_selected_iter (tree_view, &model, &iter)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + uri = katze_item_get_uri (item); + if (uri && *uri) + _midori_browser_open_uri (browser, uri); + g_object_unref (item); + } +} + +static void +_action_history_open_tab_activate (GtkAction* action, + MidoriBrowser* browser) +{ + GtkTreeView* tree_view; + GtkTreeModel* model; + GtkTreeIter iter; + KatzeItem* item; + const gchar* uri; + gint n; + + tree_view = GTK_TREE_VIEW (browser->panel_history); + if (sokoke_tree_view_get_selected_iter (tree_view, &model, &iter)) + { + gtk_tree_model_get (model, &iter, 0, &item, -1); + uri = katze_item_get_uri (item); + if (uri && *uri) + { + n = midori_browser_add_item (browser, item); + _midori_browser_set_current_page_smartly (browser, n); + } + g_object_unref (item); + } +} + +static void +_action_history_delete_activate (GtkAction* action, + MidoriBrowser* browser) +{ + GtkTreeView* treeview; + GtkTreeModel* treemodel; + GtkTreeIter iter; + GtkTreeIter childiter; + KatzeItem* item; + KatzeItem* child; + KatzeArray* parent; + gint i, n; + + treeview = GTK_TREE_VIEW (browser->panel_history); + if (sokoke_tree_view_get_selected_iter (treeview, &treemodel, &iter)) + { + gtk_tree_model_get (treemodel, &iter, 0, &item, -1); + + if (KATZE_IS_ARRAY (item)) + { + n = katze_array_get_length (KATZE_ARRAY (item)); + for (i = 0; i < n; i++) + { + child = katze_array_get_nth_item (KATZE_ARRAY (item), 0); + katze_array_remove_item (KATZE_ARRAY (item), child); + } + parent = katze_item_get_parent (item); + katze_array_remove_item (parent, item); + while (gtk_tree_model_iter_nth_child (treemodel, &childiter, &iter, 0)) + gtk_tree_store_remove (GTK_TREE_STORE (treemodel), &childiter); + gtk_tree_store_remove (GTK_TREE_STORE (treemodel), &iter); + g_object_unref (item); + } + else + { + parent = katze_item_get_parent (item); + katze_array_remove_item (parent, item); + gtk_tree_store_remove (GTK_TREE_STORE (treemodel), &iter); + g_object_unref (item); + } + } +} + +static void +_action_history_clear_activate (GtkAction* action, + MidoriBrowser* browser) +{ + GtkTreeView* tree_view; + GtkTreeStore* store; + KatzeItem* item; + gint i, n; + + if (!browser->history) + return; + + tree_view = GTK_TREE_VIEW (browser->panel_history); + store = GTK_TREE_STORE (gtk_tree_view_get_model (tree_view)); + gtk_tree_store_clear (store); + + n = katze_array_get_length (browser->history); + for (i = 0; i < n; i++) + { + item = katze_array_get_nth_item (browser->history, i); + katze_array_clear (KATZE_ARRAY (item)); + } + katze_array_clear (browser->history); +} + static void _action_undo_tab_close_activate (GtkAction* action, MidoriBrowser* browser) @@ -2601,7 +2907,18 @@ static const GtkActionEntry entries[] = { { "BookmarkDelete", GTK_STOCK_DELETE, NULL, "", N_("Delete the selected bookmark"), G_CALLBACK (_action_bookmark_delete_activate) }, - + { "HistoryDelete", GTK_STOCK_DELETE, + NULL, "", + N_("Delete the selected history item"), G_CALLBACK (_action_history_delete_activate) }, + { "HistoryClear", GTK_STOCK_CLEAR, + NULL, "", + N_("Clear the enitre history"), G_CALLBACK (_action_history_clear_activate) }, + { "HistoryOpen", GTK_STOCK_OPEN, + NULL, "", + N_("Open the selected history item"), G_CALLBACK (_action_history_open_activate) }, + { "HistoryOpenTab", STOCK_TAB_NEW, + N_("Open in New _Tab"), "", + N_("Open the selected history item in a new tab"), G_CALLBACK (_action_history_open_tab_activate) }, { "Tools", NULL, N_("_Tools") }, { "ManageSearchEngines", GTK_STOCK_PROPERTIES, N_("_Manage Search Engines"), "s", @@ -2822,6 +3139,16 @@ static const gchar* ui_markup = "" "" "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" ""; static void @@ -2862,6 +3189,7 @@ midori_browser_init (MidoriBrowser* browser) browser->bookmarks = NULL; browser->trash = NULL; browser->search_engines = NULL; + browser->history = NULL; /* Setup the window metrics */ g_signal_connect (browser, "realize", @@ -2987,6 +3315,9 @@ midori_browser_init (MidoriBrowser* browser) browser->popup_bookmark = gtk_ui_manager_get_widget ( ui_manager, "/popup_bookmark"); g_object_ref (browser->popup_bookmark); + browser->popup_history = gtk_ui_manager_get_widget ( + ui_manager, "/popup_history"); + g_object_ref (browser->popup_history); browser->menu_tools = gtk_menu_item_get_submenu (GTK_MENU_ITEM ( gtk_ui_manager_get_widget (ui_manager, "/menubar/Tools"))); menuitem = gtk_separator_menu_item_new (); @@ -3126,10 +3457,39 @@ midori_browser_init (MidoriBrowser* browser) STOCK_CONSOLE, _("Console")); /* History */ - panel = midori_view_new (); - gtk_widget_show (panel); + box = gtk_vbox_new (FALSE, 0); + treestore = gtk_tree_store_new (1, KATZE_TYPE_ITEM); + treeview = gtk_tree_view_new_with_model (GTK_TREE_MODEL (treestore)); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE); + column = gtk_tree_view_column_new (); + renderer_pixbuf = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, renderer_pixbuf, FALSE); + gtk_tree_view_column_set_cell_data_func (column, renderer_pixbuf, + (GtkTreeCellDataFunc)midori_browser_history_render_icon_cb, + treeview, NULL); + renderer_text = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, renderer_text, FALSE); + gtk_tree_view_column_set_cell_data_func (column, renderer_text, + (GtkTreeCellDataFunc)midori_browser_history_render_text_cb, + treeview, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); + g_object_unref (treestore); + g_object_connect (treeview, + "signal::row-activated", + midori_panel_history_row_activated_cb, browser, + "signal::button-release-event", + midori_panel_history_button_release_event_cb, browser, + "signal::popup-menu", + midori_panel_history_popup_menu_cb, browser, + NULL); + gtk_box_pack_start (GTK_BOX (box), treeview, TRUE, TRUE, 0); + browser->panel_history = treeview; + gtk_widget_show_all (box); + toolbar = gtk_ui_manager_get_widget (ui_manager, "/toolbar_history"); + gtk_toolbar_set_icon_size (GTK_TOOLBAR (toolbar), GTK_ICON_SIZE_MENU); + gtk_widget_show (toolbar); midori_panel_append_page (MIDORI_PANEL (browser->panel), - panel, NULL, + box, toolbar, STOCK_HISTORY, _("History")); /* Pageholder */ @@ -3298,6 +3658,8 @@ midori_browser_finalize (GObject* object) g_object_unref (browser->trash); if (browser->search_engines) g_object_unref (browser->search_engines); + if (browser->history) + g_object_unref (browser->history); G_OBJECT_CLASS (midori_browser_parent_class)->finalize (object); } @@ -3513,6 +3875,115 @@ midori_browser_load_bookmarks (MidoriBrowser* browser) _action_set_sensitive (browser, "BookmarkAdd", TRUE); } +static void +_tree_store_insert_history_item (GtkTreeStore* treestore, + GtkTreeIter* parent, + KatzeItem* item) +{ + GtkTreeIter iter; + KatzeItem* child; + guint i, n; + GtkTreeIter* piter; + + g_return_if_fail (KATZE_IS_ITEM (item)); + + if (KATZE_IS_ARRAY (item)) + { + piter = parent; + if (katze_item_get_added (item)) + { + gtk_tree_store_insert_with_values (treestore, &iter, parent, 0, 0, item, -1); + g_object_unref (item); + piter = &iter; + } + n = katze_array_get_length (KATZE_ARRAY (item)); + for (i = 0; i < n; i++) + { + child = katze_array_get_nth_item (KATZE_ARRAY (item), i); + _tree_store_insert_history_item (treestore, piter, child); + } + } + else + { + gtk_tree_store_insert_with_values (treestore, &iter, parent, 0, 0, item, -1); + g_object_unref (item); + } +} + +static void +midori_browser_new_history_item (MidoriBrowser* browser, + KatzeItem* item) +{ + GtkTreeView* treeview; + GtkTreeModel* treemodel; + GtkTreeIter iter; + KatzeArray* parent; + gint i; + gboolean found; + time_t now; + gchar newdate [70]; + gchar *today; + gsize len; + + treeview = GTK_TREE_VIEW (browser->panel_history); + treemodel = gtk_tree_view_get_model (treeview); + + now = time (NULL); + strftime (newdate, sizeof (newdate), "%Y-%m-%d %H:%M:%S", localtime (&now)); + katze_item_set_added (item, newdate); + + len = (g_strrstr (newdate, " ") - newdate); + today = g_strndup (newdate, len); + + found = FALSE; + i = 0; + while (gtk_tree_model_iter_nth_child (treemodel, &iter, NULL, i++)) + { + gtk_tree_model_get (treemodel, &iter, 0, &parent, -1); + if (g_ascii_strcasecmp (today, + katze_item_get_added (KATZE_ITEM (parent))) == 0) + { + found = TRUE; + break; + } + g_object_unref (parent); + } + if (!found) + { + parent = katze_array_new (KATZE_TYPE_ARRAY); + katze_item_set_added (KATZE_ITEM (parent), today); + katze_array_add_item (browser->history, parent); + katze_array_add_item (parent, item); + _tree_store_insert_history_item (GTK_TREE_STORE (treemodel), NULL, + KATZE_ITEM (parent)); + } + else + { + _tree_store_insert_history_item (GTK_TREE_STORE (treemodel), + &iter, item); + katze_array_add_item (parent, item); + g_object_unref (parent); + } + g_free (today); + +} + +static void +midori_browser_load_history (MidoriBrowser* browser) +{ + GtkTreeView* treeview; + GtkTreeModel* treemodel; + + if (!browser->history) + return; + + treeview = GTK_TREE_VIEW (browser->panel_history); + treemodel = gtk_tree_view_get_model (treeview); + + _tree_store_insert_history_item (GTK_TREE_STORE (treemodel), + NULL, KATZE_ITEM (browser->history)); +} + static void midori_browser_set_property (GObject* object, guint prop_id, @@ -3578,6 +4049,12 @@ midori_browser_set_property (GObject* object, _action_by_name (browser, "Search")), item); } break; + case PROP_HISTORY: + ; /* FIXME: Disconnect handlers */ + katze_object_assign (browser->history, g_value_get_object (value)); + midori_browser_load_history (browser); + /* FIXME: Connect to updates */ + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -3624,6 +4101,9 @@ midori_browser_get_property (GObject* object, case PROP_SEARCH_ENGINES: g_value_set_object (value, browser->search_engines); break; + case PROP_HISTORY: + g_value_set_object (value, browser->history); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; diff --git a/midori/wscript_build b/midori/wscript_build index cc6435cf..0dfb01a3 100644 --- a/midori/wscript_build +++ b/midori/wscript_build @@ -5,5 +5,5 @@ obj = bld.create_obj ('cc', 'program') obj.target = 'midori' obj.includes = '.. ../katze' obj.find_sources_in_dirs ('.') -obj.uselib = 'UNIQUE GIO GTK GTKSOURCEVIEW WEBKIT LIBXML' +obj.uselib = 'UNIQUE GIO GTK GTKSOURCEVIEW SQLITE WEBKIT LIBXML' obj.uselib_local = 'katze' diff --git a/wscript b/wscript index 782d8174..76e1c4da 100644 --- a/wscript +++ b/wscript @@ -110,6 +110,7 @@ def configure (conf): conf.check_pkg ('gtk+-2.0', destvar='GTK', vnum='2.6.0', mandatory=True) conf.check_pkg ('gtksourceview-2.0', destvar='GTKSOURCEVIEW', vnum='2.0', mandatory=False) + conf.check_pkg ('sqlite3', destvar='SQLITE', vnum='3.0', mandatory=False) conf.check_pkg ('webkit-1.0', destvar='WEBKIT', vnum='0.1', mandatory=True) conf.check_pkg ('libxml-2.0', destvar='LIBXML', vnum='2.6', mandatory=True) -- 2.39.5