/* Copyright (C) 2013 Paweł Forysiuk 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 ClipNotes { Midori.Database database; unowned Sqlite.Database db; Gtk.ListStore notes_list_store; Note? current_note; class Note : GLib.Object { public int64 id { get; set; } public string title { get; set; } public string? uri { get; set; default = null; } public string content { get; set; default = ""; } public void add (string title, string? uri, string note_content) { GLib.DateTime time = new DateTime.now_local (); string sqlcmd = "INSERT INTO `notes` (`uri`, `title`, `note_content`, `tstamp` ) VALUES (:uri, :title, :note_content, :tstamp);"; Midori.DatabaseStatement statement; try { statement = database.prepare (sqlcmd, ":uri", typeof (string), uri, ":title", typeof (string), title, ":note_content", typeof (string), note_content, ":tstamp", typeof (int64), time.to_unix ()); statement.step (); append_note (this); } catch (Error error) { critical (_("Failed to add new note to database: %s\n"), error.message); } this.id = db.last_insert_rowid (); this.uri = uri; this.title = title; this.content = note_content; } public void remove () { string sqlcmd = "DELETE FROM `notes` WHERE id= :id;"; Midori.DatabaseStatement statement; try { statement = database.prepare (sqlcmd, ":id", typeof (int64), this.id); statement.step (); remove_note (this.id); } catch (Error error) { critical (_("Falied to remove note from database: %s\n"), error.message); } } public void rename (string new_title) { string sqlcmd = "UPDATE `notes` SET title= :title WHERE id = :id;"; Midori.DatabaseStatement statement; try { statement = database.prepare (sqlcmd, ":id", typeof (int64), this.id, ":title", typeof (string), new_title); statement.step (); } catch (Error error) { critical (_("Falied to rename note: %s\n"), error.message); } this.title = new_title; } public void update (string new_content) { string sqlcmd = "UPDATE `notes` SET note_content= :content WHERE id = :id;"; Midori.DatabaseStatement statement; try { statement = database.prepare (sqlcmd, ":id", typeof (int64), this.id, ":content", typeof (string), new_content); statement.step (); } catch (Error error) { critical (_("Falied to update note: %s\n"), error.message); } this.content = new_content; } } void append_note (Note note) { /* Strip LRE leading character */ if (note.title != null && note.title.has_prefix ("‪")) note.title = note.title.replace ("‪", ""); Gtk.TreeIter iter; notes_list_store.append (out iter); notes_list_store.set (iter, 0, note); } void remove_note (int64 id) { Gtk.TreeIter iter; if (notes_list_store.iter_children (out iter, null)) { do { Note note; notes_list_store.get (iter, 0, out note); if (id == note.id) { if (current_note == note) { current_note = null; } notes_list_store.remove (iter); break; } } while (notes_list_store.iter_next (ref iter)); } } private class Sidebar : Gtk.VBox, Midori.Viewable { Gtk.Toolbar? toolbar = null; Gtk.Label note_label; Gtk.TreeView notes_tree_view; Gtk.TextView note_text_view = new Gtk.TextView (); public unowned string get_stock_id () { return Gtk.STOCK_EDIT; } public unowned string get_label () { return _("Notes"); } public Gtk.Widget get_toolbar () { if (toolbar == null) { toolbar = new Gtk.Toolbar (); var new_note_button = new Gtk.ToolButton.from_stock (Gtk.STOCK_EDIT); new_note_button.label = _("New Note"); new_note_button.tooltip_text = _("Creates a new empty note, unrelated to opened pages"); new_note_button.use_underline = true; new_note_button.is_important = true; new_note_button.show (); new_note_button.clicked.connect (() => { var note = new Note (); note.add (_("New note"), null, ""); }); toolbar.insert (new_note_button, -1); } return toolbar; } internal void title_edited (Gtk.CellRendererText renderer, string? path_str, string? new_title) { var path = new Gtk.TreePath.from_string (path_str); Gtk.TreeIter iter; notes_list_store.get_iter (out iter, path); Note note; notes_list_store.get (iter, 0, out note); note.rename (new_title); notes_list_store.set (iter, 0, note); } public Sidebar () { Gtk.TreeViewColumn column; notes_list_store = new Gtk.ListStore (1, typeof (Note)); notes_tree_view = new Gtk.TreeView.with_model (notes_list_store); notes_tree_view.headers_visible = true; notes_tree_view.button_press_event.connect (button_pressed); notes_tree_view.get_selection().changed.connect (selection_changed); notes_list_store.set_sort_column_id (0, Gtk.SortType.ASCENDING); notes_list_store.set_sort_func (0, tree_sort_func); column = new Gtk.TreeViewColumn (); Gtk.CellRendererPixbuf renderer_icon = new Gtk.CellRendererPixbuf (); column.pack_start (renderer_icon, false); column.set_cell_data_func (renderer_icon, on_render_icon); notes_tree_view.append_column (column); column = new Gtk.TreeViewColumn (); Gtk.CellRendererText renderer_title = new Gtk.CellRendererText (); renderer_title.editable = true; renderer_title.edited.connect (title_edited); column.set_title (_("Notes")); column.pack_start (renderer_title, true); column.set_cell_data_func (renderer_title, on_render_note_title); notes_tree_view.append_column (column); try { string sqlcmd = "SELECT id, uri, title, note_content FROM notes"; var statement = database.prepare (sqlcmd); while (statement.step ()) { var note = new Note (); note.id = statement.get_int64 ("id"); note.uri = statement.get_string ("uri"); note.title = statement.get_string ("title"); note.content = statement.get_string ("note_content"); append_note (note); } } catch (Error error) { critical (_("Failed to select from notes database: %s\n"), error.message); } notes_tree_view.show (); pack_start (notes_tree_view, false, false, 0); note_label = new Gtk.Label (null); note_label.show (); pack_start (note_label, false, false, 0); note_text_view.set_wrap_mode (Gtk.WrapMode.WORD); note_text_view.show (); note_text_view.focus_out_event.connect (focus_lost); pack_start (note_text_view, true, true, 0); } int tree_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) { Note note1, note2; model.get (a, 0, out note1); model.get (b, 0, out note2); return strcmp (note1.title, note2.title); } void save_current_note () { if (current_note != null) { string note_content = note_text_view.buffer.text; if (note_content != current_note.content) current_note.update (note_content); } } bool focus_lost (Gdk.EventFocus event) { save_current_note (); return false; } private void on_render_note_title (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { Note note; model.get (iter, 0, out note); renderer.set ("markup", GLib.Markup.printf_escaped ("%s", note.title), "ellipsize", Pango.EllipsizeMode.END); } private void on_render_icon (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { Note note; model.get (iter, 0, out note); var pixbuf = Midori.Paths.get_icon (note.uri, null); if (pixbuf != null) { int icon_width = 16, icon_height = 16; Gtk.icon_size_lookup_for_settings (get_settings (), Gtk.IconSize.MENU, out icon_width, out icon_height); pixbuf = pixbuf.scale_simple (icon_width, icon_height, Gdk.InterpType.TILES); } renderer.set ("pixbuf", pixbuf); } private void selection_changed (Gtk.TreeSelection selection) { save_current_note (); show_note_content (selection); } bool button_pressed (Gdk.EventButton event) { if (event.button == 1) { if (event.type == Gdk.EventType.2BUTTON_PRESS) { return show_note_webpage_in_new_tab (event, false); } } if (event.button == 2) return show_note_webpage_in_new_tab (event, true); if (event.button == 3) return show_popup_menu (event); return false; } bool show_note_content (Gtk.TreeSelection selection) { Gtk.TreeIter iter; if (selection.get_selected (null, out iter)) { Note note; notes_list_store.get (iter, 0, out note); if (note != current_note) { note_text_view.buffer.text = note.content; current_note = note; } return true; } else { note_text_view.buffer.text = ""; } return false; } bool show_note_webpage_in_new_tab (Gdk.EventButton? event, bool new_tab) { Gtk.TreeIter iter; if (notes_tree_view.get_selection ().get_selected (null, out iter)) { Note note; notes_list_store.get (iter, 0, out note); if (note.uri != null) { var browser = Midori.Browser.get_for_widget (notes_tree_view); if (new_tab) { browser.add_uri (note.uri); } else { var tab = browser.tab as Midori.View; tab.set_uri (note.uri); } return true; } } return false; } bool show_popup_menu (Gdk.EventButton? event) { return_val_if_fail (event.window == notes_tree_view.get_bin_window(), false); Gtk.TreePath path = null; notes_tree_view.get_path_at_pos ((int)event.x, (int)event.y, out path, null, null, null); if (path != null) { Gtk.TreeIter iter; notes_list_store.get_iter (out iter, path); Note note; notes_list_store.get (iter, 0, out note); var menu = new Gtk.Menu (); var menuitem = new Gtk.ImageMenuItem.with_label (_("Rename note")); var image = new Gtk.Image.from_stock (Gtk.STOCK_EDIT, Gtk.IconSize.MENU); menuitem.always_show_image = true; menuitem.set_image (image); menuitem.activate.connect (() => { notes_tree_view.set_cursor (path, notes_tree_view.get_column (1), true); }); menu.append (menuitem); menuitem = new Gtk.ImageMenuItem.with_label (_("Copy note to clipboard")); image = new Gtk.Image.from_stock (Gtk.STOCK_COPY, Gtk.IconSize.MENU); menuitem.always_show_image = true; menuitem.set_image (image); menuitem.activate.connect (() => { get_clipboard (Gdk.SELECTION_CLIPBOARD).set_text (note.content, -1); }); menu.append (menuitem); menuitem = new Gtk.ImageMenuItem.with_label (_("Remove note")); image = new Gtk.Image.from_stock (Gtk.STOCK_DELETE, Gtk.IconSize.MENU); menuitem.always_show_image = true; menuitem.set_image (image); menuitem.activate.connect (() => { note.remove (); }); menu.append (menuitem); menu.show_all (); Katze.widget_popup (notes_tree_view, menu, null, Katze.MenuPos.CURSOR); return true; } return false; } } private class Manager : Midori.Extension { internal GLib.List widgets; void tab_added (Midori.Browser browser, Midori.Tab tab) { tab.context_menu.connect (add_menu_items); } void add_menu_items (Midori.Tab tab, WebKit.HitTestResult hit_test_result, Midori.ContextAction menu) { #if !HAVE_WEBKIT2 if ((hit_test_result.context & WebKit.HitTestResultContext.SELECTION) == 0) return; #endif var view = tab as Midori.View; var action = new Gtk.Action ("Notes", _("Copy selection as note"), null, null); action.activate.connect ((action)=> { if (view.has_selection () == true) { string selected_text = view.get_selected_text (); string uri = view.get_display_uri (); string title = view.get_display_title (); var note = new Note(); note.add (title, uri, selected_text); } }); menu.add (action); } void browser_added (Midori.Browser browser) { var viewable = new Sidebar (); viewable.show (); browser.panel.append_page (viewable); widgets.append (viewable); foreach (var tab in browser.get_tabs ()) tab_added (browser, tab); browser.add_tab.connect (tab_added); } void activated (Midori.App app) { string config_path = this.get_config_dir () ?? ":memory:"; string db_path = GLib.Path.build_path (Path.DIR_SEPARATOR_S, config_path, "notes.db"); try { database = new Midori.Database (db_path); } catch (Midori.DatabaseError schema_error) { error (schema_error.message); } db = database.db; widgets = new GLib.List (); app.add_browser.connect (browser_added); foreach (var browser in app.get_browsers ()) browser_added (browser); } void deactivated () { var app = get_app (); app.add_browser.disconnect (browser_added); foreach (var widget in widgets) widget.destroy (); } internal Manager () { GLib.Object (name: _("Notes"), description: _("Save text clips from websites as notes"), version: "0.1" + Midori.VERSION_SUFFIX, authors: "Paweł Forysiuk"); this.activate.connect (activated); this.deactivate.connect (deactivated); } } } public Midori.Extension extension_init () { return new ClipNotes.Manager (); }