460 lines
17 KiB
Vala
460 lines
17 KiB
Vala
|
/*
|
|||
|
Copyright (C) 2013 Paweł Forysiuk <tuxator@o2.pl>
|
|||
|
|
|||
|
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<Gtk.Widget> 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<Gtk.Widget> ();
|
|||
|
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 ();
|
|||
|
}
|