Merge lp:~midori/midori/notes-extension into lp:midori
- notes-extension
- Merge into trunk
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 |
Related bugs: |
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/
- 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 : | # |
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 |
Voting does not meet specified criteria. Required: Approve >= 1. Got: 1 Pending.