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

Subscribers

People subscribed via source and target branches

to all changes: