/* Copyright (C) 2013 André Stösel 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 Tabby { int IDLE_RESTORE_COUNT = 13; /* FixMe: don't use a global object */ Midori.App? APP; /* function called from Manager object */ public interface IStorage : GLib.Object { public abstract Katze.Array get_saved_sessions (); public abstract Base.Session get_new_session (); public abstract void restore_last_sessions (); public abstract void import_session (Katze.Array tabs); } public interface ISession : GLib.Object { public abstract Katze.Array get_tabs (); /* Add one tab to the database */ public abstract void add_item (Katze.Item item); /* Attach to a browser */ public abstract void attach (Midori.Browser browser); /* Attach to a browser and populate it with tabs from the database */ public abstract void restore (Midori.Browser browser); /* Remove all tabs from the database */ public abstract void remove (); /* Run when a browser is closed */ public abstract void close (); } public enum SessionState { OPEN, CLOSED, RESTORING } namespace Base { /* each base class should connect to all necessary signals and provide an abstract function to handle them */ public abstract class Storage : GLib.Object, IStorage { public Midori.App app { get; construct; } public abstract Katze.Array get_saved_sessions (); public abstract Base.Session get_new_session (); public void start_new_session () { Katze.Array sessions = new Katze.Array (typeof (Session)); this.init_sessions (sessions); } public void restore_last_sessions () { Katze.Array sessions = this.get_saved_sessions (); this.init_sessions (sessions); } private void init_sessions (Katze.Array sessions) { if (sessions.is_empty ()) { sessions.add_item (this.get_new_session ()); } GLib.List items = sessions.get_items (); foreach (Katze.Item item in items) { Session session = item as Session; Midori.Browser browser = this.app.create_browser (); /* FixMe: tabby-session should be set in .restore and .attch */ browser.set_data ("tabby-session", session as Base.Session); app.add_browser (browser); browser.show (); session.restore (browser); } } public virtual void import_session (Katze.Array tabs) { Session session = this.get_new_session (); GLib.List items = tabs.get_items (); double i = 0; foreach (Katze.Item item in items) { item.set_meta_string ("sorting", i.to_string()); // See midori_browser_step_history: don't add to history item.set_meta_string ("history-step", "ignore"); i += 1024; session.add_item (item); } } } public abstract class Session : GLib.Object, ISession { protected GLib.SList tab_sorting; public Midori.Browser? browser { get; protected set; default = null; } public SessionState state { get; protected set; default = SessionState.CLOSED; } public abstract void add_item (Katze.Item item); public abstract void uri_changed (Midori.View view, string uri); public abstract void data_changed (Midori.View view); public abstract void tab_added (Midori.Browser browser, Midori.View view); public abstract void tab_removed (Midori.Browser browser, Midori.View view); public abstract void tab_switched (Midori.View? old_view, Midori.View? new_view); public abstract void tab_reordered (Gtk.Widget tab, uint pos); public abstract void remove (); public abstract Katze.Array get_tabs (); public abstract double get_max_sorting (); public void attach (Midori.Browser browser) { this.browser = browser; browser.add_tab.connect_after (this.tab_added); browser.add_tab.connect (this.helper_data_changed); browser.remove_tab.connect (this.tab_removed); browser.switch_tab.connect (this.tab_switched); browser.delete_event.connect_after(this.delete_event); browser.notebook.page_reordered.connect_after (this.tab_reordered); this.state = SessionState.OPEN; foreach (Midori.View view in browser.get_tabs ()) { this.tab_added (browser, view); this.helper_data_changed (browser, view); } } public void restore (Midori.Browser browser) { this.browser = browser; Katze.Array tabs = this.get_tabs (); unowned Katze.Array? open_uris = browser.get_data ("tabby-open-uris"); if(tabs.is_empty () && open_uris == null) { /* Using get here to avoid MidoriMidoriStartup in generated C with Vala 0.20.1 */ int load_on_startup; APP.settings.get ("load-on-startup", out load_on_startup); Katze.Item item = new Katze.Item (); if (load_on_startup == Midori.MidoriStartup.BLANK_PAGE) { item.uri = "about:dial"; } else { item.uri = "about:home"; } tabs.add_item (item); } browser.add_tab.connect_after (this.tab_added); browser.add_tab.connect (this.helper_data_changed); browser.remove_tab.connect (this.tab_removed); browser.switch_tab.connect (this.tab_switched); browser.delete_event.connect_after(this.delete_event); browser.notebook.page_reordered.connect_after (this.tab_reordered); GLib.List items = new GLib.List (); if (open_uris != null) { items.concat (open_uris.get_items ()); } items.concat (tabs.get_items ()); unowned GLib.List u_items = items; bool delay = false; bool should_delay = false; int load_on_startup; APP.settings.get ("load-on-startup", out load_on_startup); should_delay = load_on_startup == Midori.MidoriStartup.DELAYED_PAGES; if (APP.crashed == true) { delay = true; should_delay = true; } this.state = SessionState.RESTORING; GLib.Idle.add (() => { /* Note: we need to use `items` for something to maintain a valid reference */ GLib.PtrArray new_tabs = new GLib.PtrArray (); if (items.length () > 0) { for (int i = 0; i < IDLE_RESTORE_COUNT; i++) { if (u_items == null) { this.helper_reorder_tabs (new_tabs); this.state = SessionState.OPEN; return false; } Katze.Item t_item = u_items.data; t_item.set_meta_integer ("append", 1); if (delay && should_delay) t_item.set_meta_integer ("delay", Midori.Delay.DELAYED); else delay = true; unowned Gtk.Widget tab = browser.add_item (t_item); new_tabs.add (tab); u_items = u_items.next; } this.helper_reorder_tabs (new_tabs); } if (u_items == null) { this.state = SessionState.OPEN; return false; } return true; }); } public virtual void close () { if (this.state == SessionState.CLOSED) { assert (this.browser == null); } else { this.state = SessionState.CLOSED; this.browser.add_tab.disconnect (this.tab_added); this.browser.add_tab.disconnect (this.helper_data_changed); this.browser.remove_tab.disconnect (this.tab_removed); this.browser.switch_tab.disconnect (this.tab_switched); this.browser.delete_event.disconnect (this.delete_event); this.browser.notebook.page_reordered.disconnect (this.tab_reordered); this.browser = null; } } #if HAVE_GTK3 protected bool delete_event (Gtk.Widget widget, Gdk.EventAny event) { #else protected bool delete_event (Gtk.Widget widget, Gdk.Event event) { #endif this.close (); return false; } protected double get_tab_sorting (Midori.View view) { int this_pos = this.browser.notebook.page_num (view); Midori.View prev_view = this.browser.notebook.get_nth_page (this_pos - 1) as Midori.View; Midori.View next_view = this.browser.notebook.get_nth_page (this_pos + 1) as Midori.View; string prev_meta_sorting = null; string next_meta_sorting = null; double prev_sorting, next_sorting, this_sorting; if (prev_view != null) { unowned Katze.Item prev_item = prev_view.get_proxy_item (); prev_meta_sorting = prev_item.get_meta_string ("sorting"); } if (prev_meta_sorting == null) if (this.state == SessionState.RESTORING) prev_sorting = this.get_max_sorting (); else prev_sorting = double.parse ("0"); else prev_sorting = double.parse (prev_meta_sorting); if (next_view != null) { unowned Katze.Item next_item = next_view.get_proxy_item (); next_meta_sorting = next_item.get_meta_string ("sorting"); } if (next_meta_sorting == null) next_sorting = prev_sorting + 2048; else next_sorting = double.parse (next_meta_sorting); this_sorting = prev_sorting + (next_sorting - prev_sorting) / 2; return this_sorting; } private void load_status (GLib.Object _view, ParamSpec pspec) { Midori.View view = (Midori.View)_view; if (view.load_status == Midori.LoadStatus.PROVISIONAL) { unowned Katze.Item item = view.get_proxy_item (); int64 delay = item.get_meta_integer ("delay"); if (delay == Midori.Delay.UNDELAYED) { view.web_view.notify["uri"].connect ( () => { this.uri_changed (view, view.web_view.uri); }); view.web_view.notify["title"].connect ( () => { this.data_changed (view); }); } view.notify["load-status"].disconnect (load_status); } } private void helper_data_changed (Midori.Browser browser, Midori.View view) { view.notify["load-status"].connect (load_status); view.new_view.connect (this.helper_duplicate_tab); } private void helper_reorder_tabs (GLib.PtrArray new_tabs) { CompareDataFunc helper_compare_data = (a, b) => { if (a > b) return 1; else if(a < b) return -1; return 0; }; GLib.CompareFunc helper_compare_func = (a,b) => { return a == b ? 0 : -1; }; this.browser.notebook.page_reordered.disconnect (this.tab_reordered); for(var i = 0; i < new_tabs.len; i++) { Midori.View tab = new_tabs.index(i) as Midori.View; unowned Katze.Item item = tab.get_proxy_item (); double sorting; string? sorting_string = item.get_meta_string ("sorting"); if (sorting_string != null) { /* we have to use a seperate if condition to avoid a `possibly unassigned local variable` error */ if (double.try_parse (item.get_meta_string ("sorting"), out sorting)) { this.tab_sorting.insert_sorted_with_data (sorting, helper_compare_data); int index = this.tab_sorting.position (this.tab_sorting.find_custom (sorting, helper_compare_func)); this.browser.notebook.reorder_child (tab, index); } } } this.browser.notebook.page_reordered.connect_after (this.tab_reordered); } private void helper_duplicate_tab (Midori.View view, Midori.View new_view, Midori.NewView where, bool user_initiated) { unowned Katze.Item item = view.get_proxy_item (); unowned Katze.Item new_item = new_view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); int64 new_tab_id = new_item.get_meta_integer ("tabby-id"); if (tab_id > 0 && tab_id == new_tab_id) { new_item.set_meta_integer ("tabby-id", 0); } } construct { this.tab_sorting = new GLib.SList (); } } } namespace Local { private class Session : Base.Session { public int64 id { get; private set; } private Midori.Database database; public override void add_item (Katze.Item item) { GLib.DateTime time = new DateTime.now_local (); string? sorting = item.get_meta_string ("sorting") ?? "1"; string sqlcmd = "INSERT INTO `tabs` (`crdate`, `tstamp`, `session_id`, `uri`, `title`, `sorting`) VALUES (:crdate, :tstamp, :session_id, :uri, :title, :sorting);"; int64 tstamp = item.get_meta_integer ("tabby-tstamp"); if (tstamp < 0) { // new tab without focus tstamp = 0; } try { var statement = database.prepare (sqlcmd, ":crdate", typeof (int64), time.to_unix (), ":tstamp", typeof (int64), tstamp, ":session_id", typeof (int64), this.id, ":uri", typeof (string), item.uri, ":title", typeof (string), item.name, ":sorting", typeof (double), double.parse (sorting)); statement.exec (); int64 tab_id = statement.row_id (); item.set_meta_integer ("tabby-id", tab_id); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } protected override void uri_changed (Midori.View view, string uri) { unowned Katze.Item item = view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); string sqlcmd = "UPDATE `tabs` SET uri = :uri WHERE session_id = :session_id AND id = :tab_id;"; try { database.prepare (sqlcmd, ":uri", typeof (string), uri, ":session_id", typeof (int64), this.id, ":tab_id", typeof (int64), tab_id).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } protected override void data_changed (Midori.View view) { unowned Katze.Item item = view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); string sqlcmd = "UPDATE `tabs` SET title = :title WHERE session_id = :session_id AND id = :tab_id;"; try { database.prepare (sqlcmd, ":title", typeof (string), view.get_display_title (), ":session_id", typeof (int64), this.id, ":tab_id", typeof (int64), tab_id).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } protected override void tab_added (Midori.Browser browser, Midori.View view) { unowned Katze.Item item = view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); if (tab_id < 1) { double sorting = this.get_tab_sorting (view); item.set_meta_string ("sorting", sorting.to_string ()); this.add_item (item); } } protected override void tab_removed (Midori.Browser browser, Midori.View view) { unowned Katze.Item item = view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); /* FixMe: mark as deleted */ string sqlcmd = "DELETE FROM `tabs` WHERE session_id = :session_id AND id = :tab_id;"; try { database.prepare (sqlcmd, ":session_id", typeof (int64), this.id, ":tab_id", typeof (int64), tab_id).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } protected override void tab_switched (Midori.View? old_view, Midori.View? new_view) { GLib.DateTime time = new DateTime.now_local (); unowned Katze.Item item = new_view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); int64 tstamp = time.to_unix(); item.set_meta_integer ("tabby-tstamp", tstamp); string sqlcmd = "UPDATE `tabs` SET tstamp = :tstamp WHERE session_id = :session_id AND id = :tab_id;"; try { database.prepare (sqlcmd, ":session_id", typeof (int64), this.id, ":tab_id", typeof (int64), tab_id, ":tstamp", typeof (int64), tstamp).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } protected override void tab_reordered (Gtk.Widget tab, uint pos) { Midori.View view = tab as Midori.View; double sorting = this.get_tab_sorting (view); unowned Katze.Item item = view.get_proxy_item (); int64 tab_id = item.get_meta_integer ("tabby-id"); string sqlcmd = "UPDATE `tabs` SET sorting = :sorting WHERE session_id = :session_id AND id = :tab_id;"; try { database.prepare (sqlcmd, ":session_id", typeof (int64), this.id, ":tab_id", typeof (int64), tab_id, ":sorting", typeof (double), sorting).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } item.set_meta_string ("sorting", sorting.to_string ()); } public override void remove() { string sqlcmd = """ DELETE FROM `tabs` WHERE session_id = :session_id; DELETE FROM `sessions` WHERE id = :session_id; """; try { database.prepare (sqlcmd, ":session_id", typeof (int64), this.id). exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } public override void close () { /* base.close may unset this.browser, so hold onto it */ Midori.Browser? my_browser = this.browser; base.close (); bool should_break = true; if (my_browser != null && !my_browser.destroy_with_parent) { foreach (Midori.Browser browser in APP.get_browsers ()) { if (browser != my_browser && !browser.destroy_with_parent) { should_break = false; break; } } if (should_break) { return; } } GLib.DateTime time = new DateTime.now_local (); string sqlcmd = "UPDATE `sessions` SET closed = 1, tstamp = :tstamp WHERE id = :session_id;"; try { database.prepare (sqlcmd, ":session_id", typeof (int64), this.id, ":tstamp", typeof (int64), time.to_unix ()).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } public override Katze.Array get_tabs() { Katze.Array tabs = new Katze.Array (typeof (Katze.Item)); string sqlcmd = "SELECT id, uri, title, sorting FROM tabs WHERE session_id = :session_id ORDER BY tstamp DESC"; try { var statement = database.prepare (sqlcmd, ":session_id", typeof (int64), this.id); while (statement.step ()) { Katze.Item item = new Katze.Item (); int64 id = statement.get_int64 ("id"); string uri = statement.get_string ("uri"); string title = statement.get_string ("title"); double sorting = statement.get_double ("sorting"); item.uri = uri; item.name = title; item.set_meta_integer ("tabby-id", id); item.set_meta_string ("sorting", sorting.to_string ()); // See midori_browser_step_history: don't add to history item.set_meta_string ("history-step", "ignore"); tabs.add_item (item); } } catch (Error error) { critical (_("Failed to select from database: %s"), error.message); } return tabs; } public override double get_max_sorting () { string sqlcmd = "SELECT MAX(sorting) FROM tabs WHERE session_id = :session_id"; try { var statement = database.prepare (sqlcmd, ":session_id", typeof (int64), this.id); statement.step (); double sorting = statement.get_double ("MAX(sorting)"); if (!sorting.is_nan ()) { return sorting; } } catch (Error error) { critical (_("Failed to select from database: %s"), error.message); } return 0.0; } internal Session (Midori.Database database) { this.database = database; GLib.DateTime time = new DateTime.now_local (); string sqlcmd = "INSERT INTO `sessions` (`tstamp`) VALUES (:tstamp);"; try { var statement = database.prepare (sqlcmd, ":tstamp", typeof (int64), time.to_unix ()); statement.exec (); this.id = statement.row_id (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } internal Session.with_id (Midori.Database database, int64 id) { this.database = database; this.id = id; GLib.DateTime time = new DateTime.now_local (); string sqlcmd = "UPDATE `sessions` SET closed = 0, tstamp = :tstamp WHERE id = :session_id;"; try { database.prepare (sqlcmd, ":session_id", typeof (int64), this.id, ":tstamp", typeof (int64), time.to_unix ()).exec (); } catch (Error error) { critical (_("Failed to update database: %s"), error.message); } } } private class Storage : Base.Storage { private Midori.Database database; public override Katze.Array get_saved_sessions () { Katze.Array sessions = new Katze.Array (typeof (Session)); string sqlcmd = """ SELECT id, closed FROM sessions WHERE closed = 0 UNION SELECT * FROM (SELECT id, closed FROM sessions WHERE closed = 1 ORDER BY tstamp DESC LIMIT 1) ORDER BY closed; """; try { var statement = database.prepare (sqlcmd); while (statement.step ()) { int64 id = statement.get_int64 ("id"); int64 closed = statement.get_int64 ("closed"); if (closed == 0 || sessions.is_empty ()) { sessions.add_item (new Session.with_id (this.database, id)); } } } catch (Error error) { critical (_("Failed to select from database: %s"), error.message); } if (sessions.is_empty ()) { sessions.add_item (new Session (this.database)); } return sessions; } public override void import_session (Katze.Array tabs) { try { database.transaction (()=>{ base.import_session(tabs); return true; }); } catch (Error error) { critical (_("Failed to select from database: %s"), error.message); } } public override Base.Session get_new_session () { return new Session (this.database) as Base.Session; } internal Storage (Midori.App app) { GLib.Object (app: app); try { database = new Midori.Database ("tabby.db"); } catch (Midori.DatabaseError schema_error) { error (schema_error.message); } if (database.first_use) { string config_file = Midori.Paths.get_config_filename_for_reading ("session.xbel"); try { Katze.Array old_session = new Katze.Array (typeof (Katze.Item)); Midori.array_from_file (old_session, config_file, "xbel-tiny"); this.import_session (old_session); } catch (GLib.FileError file_error) { /* no old session.xbel -> could be a new profile -> ignore it */ } catch (GLib.Error error) { critical (_("Failed to import legacy session: %s"), error.message); } } } } } private class Manager : Midori.Extension { private Base.Storage storage; private bool load_session () { /* Using get here to avoid MidoriMidoriStartup in generated C with Vala 0.20.1 */ int load_on_startup; APP.settings.get ("load-on-startup", out load_on_startup); if (load_on_startup == Midori.MidoriStartup.BLANK_PAGE || load_on_startup == Midori.MidoriStartup.HOMEPAGE) { this.storage.start_new_session (); } else { this.storage.restore_last_sessions (); } /* FIXME: execute_commands should be called before session creation */ GLib.Idle.add (this.execute_commands); return false; } private bool execute_commands () { Midori.App app = this.get_app (); unowned string?[] commands = app.get_data ("execute-commands"); if (commands != null) { app.send_command (commands); } return false; } private void set_open_uris (Midori.Browser browser) { Midori.App app = this.get_app (); unowned string?[] uris = app.get_data ("open-uris"); if (uris != null) { Katze.Array tabs = new Katze.Array (typeof (Katze.Item)); for(int i = 0; uris[i] != null; i++) { Katze.Item item = new Katze.Item (); item.name = uris[i]; item.uri = Midori.Sokoke.magic_uri (uris[i], true, true); if (item.uri != null) { tabs.add_item (item); } } if (!tabs.is_empty()) { browser.set_data ("tabby-open-uris", tabs); } } app.add_browser.disconnect (this.set_open_uris); } private void browser_added (Midori.Browser browser) { Base.Session? session = browser.get_data ("tabby-session"); if (session == null) { session = this.storage.get_new_session () as Base.Session; browser.set_data ("tabby-session", session); session.attach (browser); } } private void browser_removed (Midori.Browser browser) { Base.Session? session = browser.get_data ("tabby-session"); if (session == null) { GLib.warning ("missing session"); } else { session.close (); /* Using get here to avoid MidoriMidoriStartup in generated C with Vala 0.20.1 */ int load_on_startup; APP.settings.get ("load-on-startup", out load_on_startup); if (browser.destroy_with_parent || load_on_startup < Midori.MidoriStartup.LAST_OPEN_PAGES) { /* Remove js popups and close if not restoring on startup */ session.remove (); } } } private void activated (Midori.App app) { APP = app; unowned string? restore_count = GLib.Environment.get_variable ("TABBY_RESTORE_COUNT"); if (restore_count != null) { int count = int.parse (restore_count); if (count >= 1) { IDLE_RESTORE_COUNT = count; } } /* FixMe: provide an option to replace Local.Storage with IStorage based Objects */ this.storage = new Local.Storage (this.get_app ()) as Base.Storage; app.add_browser.connect (this.set_open_uris); app.add_browser.connect (this.browser_added); app.remove_browser.connect (this.browser_removed); GLib.Idle.add (this.load_session); } private void deactivated () { /* set_open_uris will disconnect itself if called, but it may have been called before we are deactivated */ APP.add_browser.disconnect (this.set_open_uris); APP.add_browser.disconnect (this.browser_added); APP.remove_browser.disconnect (this.browser_removed); APP = null; this.storage = null; } internal Manager () { GLib.Object (name: _("Tabby"), description: _("Tab and session management."), version: "0.1", authors: "André Stösel "); this.activate.connect (this.activated); this.deactivate.connect (this.deactivated); } } } public Midori.Extension extension_init () { return new Tabby.Manager (); }