Merge lp:~midori/midori/notes-extension into lp:midori

Proposed by Paweł Forysiuk
Status: Merged
Approved by: Cris Dywan
Approved revision: 6539
Merged at revision: 6548
Proposed branch: lp:~midori/midori/notes-extension
Merge into: lp:midori
Diff against target: 520 lines (+479/-1)
5 files modified
data/notes/Create.sql (+8/-0)
extensions/notes.vala (+467/-0)
midori/midori-database.vala (+2/-1)
midori/midori.vapi (+1/-0)
po/POTFILES.in (+1/-0)
To merge this branch: bzr merge lp:~midori/midori/notes-extension
Reviewer Review Type Date Requested Status
Cris Dywan Approve
Review via email: mp+196160@code.launchpad.net

Commit message

Implement notes extension

Description of the change

This is a work in progress. Could use some refactoring too.

"Works":
- adding selected text to notes from websites (not in frames - unrelated bug)
- removing note
- renaming note
- adding non-webpage specific note from sidepanel
- adding/changing note content
- show note on 1 click
- go to page on double click
- sorting by title

FIXME:
- resizing/ellipsizing of widgets
- no feedback when saving note

Wishlist/TODO:
- copy note content to pastebin?
- Support importing/editing xfce notes?
- Support importing opera notes?
- notes should have tags?
- some kind of searching?

To post a comment you must log in.
Revision history for this message
RabbitBot (rabbitbot-a) wrote :

Voting does not meet specified criteria. Required: Approve >= 1. Got: 1 Pending.

Revision history for this message
Cris Dywan (kalikiana) wrote :

I do.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'data/notes'
2=== added file 'data/notes/Create.sql'
3--- data/notes/Create.sql 1970-01-01 00:00:00 +0000
4+++ data/notes/Create.sql 2014-01-26 22:03:24 +0000
5@@ -0,0 +1,8 @@
6+CREATE TABLE IF NOT EXISTS notes
7+(
8+ id INTEGER PRIMARY KEY,
9+ uri TEXT,
10+ title TEXT,
11+ note_content TEXT,
12+ tstamp INTEGER
13+);
14
15=== added file 'extensions/notes.vala'
16--- extensions/notes.vala 1970-01-01 00:00:00 +0000
17+++ extensions/notes.vala 2014-01-26 22:03:24 +0000
18@@ -0,0 +1,467 @@
19+/*
20+ Copyright (C) 2013 Paweł Forysiuk <tuxator@o2.pl>
21+
22+ This library is free software; you can redistribute it and/or
23+ modify it under the terms of the GNU Lesser General Public
24+ License as published by the Free Software Foundation; either
25+ version 2.1 of the License, or (at your option) any later version.
26+
27+ See the file COPYING for the full license text.
28+*/
29+
30+using Gtk;
31+using Midori;
32+using WebKit;
33+using Sqlite;
34+
35+namespace ClipNotes {
36+
37+ Midori.Database database;
38+ unowned Sqlite.Database db;
39+ Gtk.ListStore notes_list_store;
40+ int64 last_used_id;
41+
42+ class Note : GLib.Object {
43+ public int64 id { get; set; }
44+ public string title { get; set; }
45+ public string? uri { get; set; default = null; }
46+ public string content { get; set; default = ""; }
47+
48+ public void add (string title, string? uri, string note_content)
49+ {
50+ GLib.DateTime time = new DateTime.now_local ();
51+ string sqlcmd = "INSERT INTO `notes` (`uri`, `title`, `note_content`, `tstamp` ) VALUES (:uri, :title, :note_content, :tstamp);";
52+ Midori.DatabaseStatement statement;
53+ try {
54+ statement = database.prepare (sqlcmd,
55+ ":uri", typeof (string), uri,
56+ ":title", typeof (string), title,
57+ ":note_content", typeof (string), note_content,
58+ ":tstamp", typeof (int64), time.to_unix ());
59+
60+ statement.step ();
61+
62+ append_note (this);
63+ } catch (Error error) {
64+ critical (_("Failed to add new note to database: %s\n"), error.message);
65+ }
66+
67+ this.id = db.last_insert_rowid ();
68+ this.uri = uri;
69+ this.title = title;
70+ this.content = note_content;
71+ }
72+
73+ public void remove ()
74+ {
75+ string sqlcmd = "DELETE FROM `notes` WHERE id= :id;";
76+ Midori.DatabaseStatement statement;
77+ try {
78+ statement = database.prepare (sqlcmd,
79+ ":id", typeof (int64), this.id);
80+
81+ statement.step ();
82+ remove_note (this.id);
83+ } catch (Error error) {
84+ critical (_("Falied to remove note from database: %s\n"), error.message);
85+ }
86+ }
87+
88+ public void rename (string new_title)
89+ {
90+ string sqlcmd = "UPDATE `notes` SET title= :title WHERE id = :id;";
91+ Midori.DatabaseStatement statement;
92+ try {
93+ statement = database.prepare (sqlcmd,
94+ ":id", typeof (int64), this.id,
95+ ":title", typeof (string), new_title);
96+ statement.step ();
97+ } catch (Error error) {
98+ critical (_("Falied to rename note: %s\n"), error.message);
99+ }
100+
101+ this.title = new_title;
102+ }
103+
104+ public void update (string new_content)
105+ {
106+ string sqlcmd = "UPDATE `notes` SET note_content= :content WHERE id = :id;";
107+ Midori.DatabaseStatement statement;
108+ try {
109+ statement = database.prepare (sqlcmd,
110+ ":id", typeof (int64), this.id,
111+ ":content", typeof (string), new_content);
112+ statement.step ();
113+ } catch (Error error) {
114+ critical (_("Falied to update note: %s\n"), error.message);
115+ }
116+ this.content = new_content;
117+ }
118+ }
119+
120+ void append_note (Note note)
121+ {
122+ /* Strip LRE leading character */
123+ if (note.title != null && note.title.has_prefix ("‪"))
124+ note.title = note.title.replace ("‪", "");
125+
126+ Gtk.TreeIter iter;
127+ notes_list_store.append (out iter);
128+ notes_list_store.set (iter, 0, note);
129+ }
130+
131+ void remove_note (int64 id)
132+ {
133+ Gtk.TreeIter iter;
134+ if (notes_list_store.iter_children (out iter, null)) {
135+ do {
136+ Note note;
137+ notes_list_store.get (iter, 0, out note);
138+ if (id == note.id) {
139+ notes_list_store.remove (iter);
140+ }
141+ } while (notes_list_store.iter_next (ref iter));
142+ }
143+ }
144+
145+
146+ private class Sidebar : Gtk.VBox, Midori.Viewable {
147+ Gtk.Toolbar? toolbar = null;
148+ Gtk.Label note_label;
149+ Gtk.TreeView notes_tree_view;
150+ Gtk.TextView note_text_view = new Gtk.TextView ();
151+
152+ public unowned string get_stock_id () {
153+ return Gtk.STOCK_EDIT;
154+ }
155+
156+ public unowned string get_label () {
157+ return _("Notes");
158+ }
159+
160+ public Gtk.Widget get_toolbar () {
161+ if (toolbar == null) {
162+ toolbar = new Gtk.Toolbar ();
163+ var new_note_button = new Gtk.ToolButton.from_stock (Gtk.STOCK_EDIT);
164+ new_note_button.label = _("New Note");
165+ new_note_button.tooltip_text = _("Creates a new empty note, urelated to opened pages");
166+ new_note_button.use_underline = true;
167+ new_note_button.is_important = true;
168+ new_note_button.show ();
169+ new_note_button.clicked.connect (() => {
170+ var note = new Note ();
171+ note.add (_("New note"), null, "");
172+ });
173+ toolbar.insert (new_note_button, -1);
174+ }
175+ return toolbar;
176+ }
177+
178+ public Sidebar () {
179+ Gtk.TreeViewColumn column;
180+
181+ notes_list_store = new Gtk.ListStore (1, typeof (Note));
182+ notes_tree_view = new Gtk.TreeView.with_model (notes_list_store);
183+ notes_tree_view.headers_visible = true;
184+ notes_tree_view.button_press_event.connect (button_pressed);
185+
186+ notes_list_store.set_sort_column_id (0, Gtk.SortType.ASCENDING);
187+ notes_list_store.set_sort_func (0, tree_sort_func);
188+
189+ column = new Gtk.TreeViewColumn ();
190+ Gtk.CellRendererPixbuf renderer_icon = new Gtk.CellRendererPixbuf ();
191+ column.pack_start (renderer_icon, false);
192+ column.set_cell_data_func (renderer_icon, on_render_icon);
193+ notes_tree_view.append_column (column);
194+
195+ column = new Gtk.TreeViewColumn ();
196+ Gtk.CellRendererText renderer_title = new Gtk.CellRendererText ();
197+ column.set_title (_("Notes"));
198+ column.pack_start (renderer_title, true);
199+ column.set_cell_data_func (renderer_title, on_render_note_title);
200+ notes_tree_view.append_column (column);
201+
202+ try {
203+ string sqlcmd = "SELECT id, uri, title, note_content FROM notes";
204+ var statement = database.prepare (sqlcmd);
205+ while (statement.step ()) {
206+ var note = new Note ();
207+ note.id = statement.get_int64 ("id");
208+ note.uri = statement.get_string ("uri");
209+ note.title = statement.get_string ("title");
210+ note.content = statement.get_string ("note_content");
211+
212+ append_note (note);
213+ }
214+ } catch (Error error) {
215+ critical (_("Failed to select from notes database: %s\n"), error.message);
216+ }
217+
218+ notes_tree_view.show ();
219+ pack_start (notes_tree_view, false, false, 0);
220+
221+ note_label = new Gtk.Label (null);
222+ note_label.show ();
223+ pack_start (note_label, false, false, 0);
224+
225+ note_text_view.set_wrap_mode (Gtk.WrapMode.WORD);
226+ note_text_view.show ();
227+ note_text_view.focus_out_event.connect (focus_lost);
228+ pack_start (note_text_view, true, true, 0);
229+ }
230+
231+ int tree_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) {
232+ Note note1, note2;
233+ model.get (a, 0, out note1);
234+ model.get (b, 0, out note2);
235+ return strcmp (note1.title, note2.title);
236+ }
237+
238+ bool focus_lost (Gdk.EventFocus event) {
239+ Gtk.TreePath? path;
240+ notes_tree_view.get_cursor (out path, null);
241+ return_val_if_fail (path != null, false);
242+ Gtk.TreeIter iter;
243+ if (notes_list_store.get_iter (out iter, path)) {
244+ Note note;
245+ notes_list_store.get (iter, 0, out note);
246+ if (last_used_id == note.id) {
247+ string note_content = note_text_view.buffer.text;
248+ note.update (note_content);
249+ }
250+ }
251+ return false;
252+ }
253+
254+ private void on_render_note_title (Gtk.CellLayout column, Gtk.CellRenderer renderer,
255+ Gtk.TreeModel model, Gtk.TreeIter iter) {
256+
257+ Note note;
258+ model.get (iter, 0, out note);
259+ renderer.set ("markup", GLib.Markup.printf_escaped ("%s", note.title),
260+ "ellipsize", Pango.EllipsizeMode.END);
261+ }
262+
263+ private void on_render_icon (Gtk.CellLayout column, Gtk.CellRenderer renderer,
264+ Gtk.TreeModel model, Gtk.TreeIter iter) {
265+
266+ Note note;
267+ model.get (iter, 0, out note);
268+
269+ var pixbuf = Midori.Paths.get_icon (note.uri, null);
270+ if (pixbuf != null) {
271+ int icon_width = 16, icon_height = 16;
272+ Gtk.icon_size_lookup_for_settings (get_settings (),
273+ Gtk.IconSize.MENU, out icon_width, out icon_height);
274+ pixbuf = pixbuf.scale_simple (icon_width, icon_height, Gdk.InterpType.TILES);
275+ }
276+ renderer.set ("pixbuf", pixbuf);
277+ }
278+
279+ bool button_pressed (Gdk.EventButton event) {
280+ if (event.button == 1) {
281+ if (event.type == Gdk.EventType.2BUTTON_PRESS) {
282+ return show_note_webpage_in_new_tab (event, false);
283+ } else {
284+ return show_note_content (event);
285+ }
286+ }
287+ if (event.button == 2)
288+ return show_note_webpage_in_new_tab (event, true);
289+ if (event.button == 3)
290+ return show_popup_menu (event);
291+ return false;
292+ }
293+
294+ bool show_note_content (Gdk.EventButton? event) {
295+ Gtk.TreeIter iter;
296+ if (notes_tree_view.get_selection ().get_selected (null, out iter)) {
297+ Note note;
298+ notes_list_store.get (iter, 0, out note);
299+
300+ if (last_used_id != note.id) {
301+ note_text_view.buffer.text = note.content;
302+ last_used_id = note.id;
303+ }
304+
305+ return true;
306+ } else {
307+ note_text_view.buffer.text = "";
308+ }
309+ return false;
310+ }
311+
312+ bool show_note_webpage_in_new_tab (Gdk.EventButton? event, bool new_tab) {
313+ Gtk.TreeIter iter;
314+ if (notes_tree_view.get_selection ().get_selected (null, out iter)) {
315+ Note note;
316+ notes_list_store.get (iter, 0, out note);
317+ if (note.uri != null) {
318+ var browser = Midori.Browser.get_for_widget (notes_tree_view);
319+ if (new_tab) {
320+ browser.add_uri (note.uri);
321+ } else {
322+ var tab = browser.tab as Midori.View;
323+ tab.set_uri (note.uri);
324+ }
325+ return true;
326+ }
327+ }
328+ return false;
329+ }
330+
331+ bool show_popup_menu (Gdk.EventButton? event) {
332+ Gtk.TreeIter iter;
333+ if (notes_tree_view.get_selection ().get_selected (null, out iter)) {
334+
335+ var menu = new Gtk.Menu ();
336+
337+ var menuitem = new Gtk.ImageMenuItem.with_label (_("Rename note"));
338+ var image = new Gtk.Image.from_stock (Gtk.STOCK_EDIT, Gtk.IconSize.MENU);
339+ menuitem.always_show_image = true;
340+ menuitem.set_image (image);
341+ menuitem.activate.connect (() => {
342+ Note note;
343+ notes_list_store.get (iter, 0, out note);
344+
345+ var dialog = new Gtk.Dialog. with_buttons (_("Rename note"), null,
346+ Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL,
347+ Gtk.Stock.OK, Gtk.ResponseType.OK);
348+ Gtk.Box content = (Gtk.Box) dialog.get_content_area ();
349+ dialog.set_default_response (Gtk.ResponseType.OK);
350+ dialog.resizable = false;
351+ dialog.icon_name = Gtk.STOCK_EDIT;
352+
353+ var entry = new Gtk.Entry ();
354+ entry.text = note.title;
355+ entry.activates_default = true;
356+ content.add (entry);
357+ content.show_all ();
358+
359+ int response = dialog.run ();
360+ dialog.hide ();
361+ if (response == Gtk.ResponseType.OK) {
362+ string new_title = entry.text;
363+ if (entry.text != null && new_title != note.title) {
364+ note.rename (new_title);
365+ notes_list_store.set (iter, 0, note);
366+ }
367+ }
368+ dialog.destroy ();
369+
370+ });
371+ menu.append (menuitem);
372+
373+
374+ menuitem = new Gtk.ImageMenuItem.with_label (_("Copy note to clipboard"));
375+ image = new Gtk.Image.from_stock (Gtk.STOCK_COPY, Gtk.IconSize.MENU);
376+ menuitem.always_show_image = true;
377+ menuitem.set_image (image);
378+ menuitem.activate.connect (() => {
379+ Note note;
380+ notes_list_store.get (iter, 0, out note);
381+ get_clipboard (Gdk.SELECTION_CLIPBOARD).set_text (note.content, -1);
382+ });
383+ menu.append (menuitem);
384+
385+
386+ menuitem = new Gtk.ImageMenuItem.with_label (_("Remove note"));
387+ image = new Gtk.Image.from_stock (Gtk.STOCK_DELETE, Gtk.IconSize.MENU);
388+ menuitem.always_show_image = true;
389+ menuitem.set_image (image);
390+ menuitem.activate.connect (() => {
391+ Note note;
392+ notes_list_store.get (iter, 0, out note);
393+ note.remove ();
394+ });
395+ menu.append (menuitem);
396+
397+ menu.show_all ();
398+ Katze.widget_popup (notes_tree_view, menu, null, Katze.MenuPos.CURSOR);
399+ return true;
400+ }
401+ return false;
402+ }
403+ }
404+
405+
406+ private class Manager : Midori.Extension {
407+ internal GLib.List<Gtk.Widget> widgets;
408+
409+ void tab_added (Midori.Browser browser, Midori.Tab tab) {
410+
411+ tab.context_menu.connect (add_menu_items);
412+
413+ }
414+
415+ void add_menu_items (Midori.Tab tab, WebKit.HitTestResult hit_test_result, Midori.ContextAction menu) {
416+ if ((hit_test_result.context & WebKit.HitTestResultContext.SELECTION) == 0)
417+ return;
418+
419+ var view = tab as Midori.View;
420+ var action = new Gtk.Action ("Notes", _("Copy selection as note"), null, null);
421+ action.activate.connect ((action)=> {
422+ if (view.has_selection () == true)
423+ {
424+ string selected_text = view.get_selected_text ();
425+ string uri = view.get_display_uri ();
426+ string title = view.get_display_title ();
427+ var note = new Note();
428+ note.add (title, uri, selected_text);
429+ }
430+ });
431+
432+ menu.add (action);
433+ }
434+
435+ void browser_added (Midori.Browser browser) {
436+ var viewable = new Sidebar ();
437+ viewable.show ();
438+ browser.panel.append_page (viewable);
439+ widgets.append (viewable);
440+
441+ foreach (var tab in browser.get_tabs ())
442+ tab_added (browser, tab);
443+
444+ browser.add_tab.connect (tab_added);
445+ }
446+
447+ void activated (Midori.App app) {
448+ string? config_path = this.get_config_dir ();
449+ string? db_path = config_path != null ? GLib.Path.build_path (Path.DIR_SEPARATOR_S, config_path, "notes.db") : null;
450+ try {
451+ database = new Midori.Database (db_path);
452+ } catch (Midori.DatabaseError schema_error) {
453+ error (schema_error.message);
454+ }
455+ db = database.db;
456+
457+ widgets = new GLib.List<Gtk.Widget> ();
458+ app.add_browser.connect (browser_added);
459+ foreach (var browser in app.get_browsers ())
460+ browser_added (browser);
461+ }
462+
463+ void deactivated () {
464+ var app = get_app ();
465+ app.add_browser.disconnect (browser_added);
466+ foreach (var widget in widgets)
467+ widget.destroy ();
468+ }
469+
470+ internal Manager () {
471+ GLib.Object (name: _("Notes"),
472+ description: _("Save text clips from websites as notes"),
473+ version: "0.1" + Midori.VERSION_SUFFIX,
474+ authors: "Paweł Forysiuk");
475+
476+ this.activate.connect (activated);
477+ this.deactivate.connect (deactivated);
478+ }
479+ }
480+
481+}
482+
483+public Midori.Extension extension_init () {
484+ return new ClipNotes.Manager ();
485+}
486
487=== modified file 'midori/midori-database.vala'
488--- midori/midori-database.vala 2013-12-19 19:58:53 +0000
489+++ midori/midori-database.vala 2014-01-26 22:03:24 +0000
490@@ -95,7 +95,8 @@
491 */
492 public string? get_string (string name) throws DatabaseError {
493 int index = column_index (name);
494- if (stmt.column_type (index) != Sqlite.TEXT)
495+ int type = stmt.column_type (index);
496+ if (stmt.column_type (index) != Sqlite.TEXT && type != Sqlite.NULL)
497 throw new DatabaseError.TYPE ("Getting '%s' with wrong type in row: %s".printf (name, query));
498 return stmt.column_text (index);
499 }
500
501=== modified file 'midori/midori.vapi'
502--- midori/midori.vapi 2014-01-12 18:13:44 +0000
503+++ midori/midori.vapi 2014-01-26 22:03:24 +0000
504@@ -143,6 +143,7 @@
505 public void set_boolean (string name, bool value);
506 public void set_integer (string name, int value);
507 public void set_string (string name, string value);
508+ public unowned string get_config_dir ();
509
510 [NoAccessorMethod]
511 public string? stock_id { get; set; }
512
513=== modified file 'po/POTFILES.in'
514--- po/POTFILES.in 2013-12-12 19:10:59 +0000
515+++ po/POTFILES.in 2014-01-26 22:03:24 +0000
516@@ -91,3 +91,4 @@
517 extensions/transfers.vala
518 extensions/tabby.vala
519 extensions/flummi.vala
520+extensions/notes.vala

Subscribers

People subscribed via source and target branches

to all changes: