Merge lp:~gtg-user/gtg/tomboy-backend into lp:~gtg/gtg/old-trunk
- tomboy-backend
- Merge into old-trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~gtg-user/gtg/tomboy-backend |
Merge into: | lp:~gtg/gtg/old-trunk |
Prerequisite: | lp:~gtg-user/gtg/multibackends-halfgsoc_merge |
Diff against target: |
750 lines (+708/-1) 5 files modified
CHANGELOG (+1/-0) GTG/backends/backend_gnote.py (+61/-0) GTG/backends/backend_tomboy.py (+60/-0) GTG/backends/generictomboy.py (+583/-0) GTG/gtk/browser/browser.py (+3/-1) |
To merge this branch: | bzr merge lp:~gtg-user/gtg/tomboy-backend |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Lionel Dricot | Pending | ||
Gtg developers | Pending | ||
Review via email: mp+32641@code.launchpad.net |
This proposal supersedes a proposal from 2010-06-23.
This proposal has been superseded by a proposal from 2010-08-13.
Commit message
Description of the change
The tomboy backend. Any tomboy note matching a particular tag will be inserted (r/w) in GTG.
It handles gracefully the change of the "attached tags", that are the tags to which the backend is looking for. The r/w synchronization engine is included.
It's complemented with a series of testcases and exception handling (when tomboy is put under stress - ~500 notes on my laptop - it begins to drop connections on dbus).
This is just to show the code for a review. I'll need to review the docstrings and use this backend for a couple of weeks before considering it stable - it works pretty well so far-.
Lionel Dricot (ploum-deactivatedaccount) wrote : Posted in a previous version of this proposal | # |
- 882. By Luca Invernizzi
-
cherripicking files
- 883. By Luca Invernizzi
-
cherripicking files
- 884. By Luca Invernizzi
-
cherrypicking from my development branch
- 885. By Luca Invernizzi
-
merge w/ trunk
- 886. By Luca Invernizzi
-
small typo in docs:
- 887. By Luca Invernizzi
-
merge w/ trunk
- 888. By Luca Invernizzi
-
cherrypicking from my development branch
Unmerged revisions
Preview Diff
1 | === modified file 'CHANGELOG' |
2 | --- CHANGELOG 2010-08-04 00:30:22 +0000 |
3 | +++ CHANGELOG 2010-08-13 23:44:44 +0000 |
4 | @@ -4,6 +4,7 @@ |
5 | * Fixed bug with data consistency #579189, by Marko Kevac |
6 | * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij |
7 | * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul |
8 | + * New Tomboy/Gnote backend, by Luca Invernizzi |
9 | |
10 | 2010-03-01 Getting Things GNOME! 0.2.2 |
11 | * Autostart on login, by Luca Invernizzi |
12 | |
13 | === added file 'GTG/backends/backend_gnote.py' |
14 | --- GTG/backends/backend_gnote.py 1970-01-01 00:00:00 +0000 |
15 | +++ GTG/backends/backend_gnote.py 2010-08-13 23:44:44 +0000 |
16 | @@ -0,0 +1,61 @@ |
17 | +# -*- coding: utf-8 -*- |
18 | +# ----------------------------------------------------------------------------- |
19 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
20 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
21 | +# |
22 | +# This program is free software: you can redistribute it and/or modify it under |
23 | +# the terms of the GNU General Public License as published by the Free Software |
24 | +# Foundation, either version 3 of the License, or (at your option) any later |
25 | +# version. |
26 | +# |
27 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
28 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
29 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
30 | +# details. |
31 | +# |
32 | +# You should have received a copy of the GNU General Public License along with |
33 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
34 | +# ----------------------------------------------------------------------------- |
35 | + |
36 | +''' |
37 | +The gnote backend. The actual backend is all in GenericTomboy, since it's |
38 | +shared with the tomboy backend. |
39 | +''' |
40 | +#To introspect tomboy: qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl |
41 | + |
42 | +from GTG.backends.genericbackend import GenericBackend |
43 | +from GTG import _ |
44 | +from GTG.backends.generictomboy import GenericTomboy |
45 | + |
46 | + |
47 | + |
48 | +class Backend(GenericTomboy): |
49 | + ''' |
50 | + A simple class that adds some description to the GenericTomboy class. |
51 | + It's done this way since Tomboy and Gnote backends have different |
52 | + descriptions and Dbus addresses but the same backend behind them. |
53 | + ''' |
54 | + |
55 | + |
56 | + _general_description = { \ |
57 | + GenericBackend.BACKEND_NAME: "backend_gnote", \ |
58 | + GenericBackend.BACKEND_HUMAN_NAME: _("Gnote"), \ |
59 | + GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \ |
60 | + GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \ |
61 | + GenericBackend.BACKEND_DESCRIPTION: \ |
62 | + _("This backend can synchronize all or part of your Gnote" |
63 | + " notes in GTG. If you decide it would be handy to" |
64 | + " have one of your notes in your TODO list, just tag it " |
65 | + "with the tag you have chosen (you'll configure it later" |
66 | + "), and it will appear in GTG."),\ |
67 | + } |
68 | + |
69 | + _static_parameters = { \ |
70 | + GenericBackend.KEY_ATTACHED_TAGS: {\ |
71 | + GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \ |
72 | + GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Gnote"]}, \ |
73 | + } |
74 | + |
75 | + _BUS_ADDRESS = ("org.gnome.Gnote", |
76 | + "/org/gnome/Gnote/RemoteControl", |
77 | + "org.gnome.Gnote.RemoteControl") |
78 | |
79 | === added file 'GTG/backends/backend_tomboy.py' |
80 | --- GTG/backends/backend_tomboy.py 1970-01-01 00:00:00 +0000 |
81 | +++ GTG/backends/backend_tomboy.py 2010-08-13 23:44:44 +0000 |
82 | @@ -0,0 +1,60 @@ |
83 | +# -*- coding: utf-8 -*- |
84 | +# ----------------------------------------------------------------------------- |
85 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
86 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
87 | +# |
88 | +# This program is free software: you can redistribute it and/or modify it under |
89 | +# the terms of the GNU General Public License as published by the Free Software |
90 | +# Foundation, either version 3 of the License, or (at your option) any later |
91 | +# version. |
92 | +# |
93 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
94 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
95 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
96 | +# details. |
97 | +# |
98 | +# You should have received a copy of the GNU General Public License along with |
99 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
100 | +# ----------------------------------------------------------------------------- |
101 | + |
102 | +''' |
103 | +The tomboy backend. The actual backend is all in GenericTomboy, since it's |
104 | +shared with the Gnote backend. |
105 | +''' |
106 | + |
107 | +from GTG.backends.genericbackend import GenericBackend |
108 | +from GTG import _ |
109 | +from GTG.backends.generictomboy import GenericTomboy |
110 | + |
111 | + |
112 | + |
113 | +class Backend(GenericTomboy): |
114 | + ''' |
115 | + A simple class that adds some description to the GenericTomboy class. |
116 | + It's done this way since Tomboy and Gnote backends have different |
117 | + descriptions and Dbus addresses but the same backend behind them. |
118 | + ''' |
119 | + |
120 | + |
121 | + _general_description = { \ |
122 | + GenericBackend.BACKEND_NAME: "backend_tomboy", \ |
123 | + GenericBackend.BACKEND_HUMAN_NAME: _("Tomboy"), \ |
124 | + GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \ |
125 | + GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \ |
126 | + GenericBackend.BACKEND_DESCRIPTION: \ |
127 | + _("This backend can synchronize all or part of your Tomboy" |
128 | + " notes in GTG. If you decide it would be handy to" |
129 | + " have one of your notes in your TODO list, just tag it " |
130 | + "with the tag you have chosen (you'll configure it later" |
131 | + "), and it will appear in GTG."),\ |
132 | + } |
133 | + |
134 | + _static_parameters = { \ |
135 | + GenericBackend.KEY_ATTACHED_TAGS: {\ |
136 | + GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \ |
137 | + GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Tomboy"]}, \ |
138 | + } |
139 | + |
140 | + _BUS_ADDRESS = ("org.gnome.Tomboy", |
141 | + "/org/gnome/Tomboy/RemoteControl", |
142 | + "org.gnome.Tomboy.RemoteControl") |
143 | |
144 | === added file 'GTG/backends/generictomboy.py' |
145 | --- GTG/backends/generictomboy.py 1970-01-01 00:00:00 +0000 |
146 | +++ GTG/backends/generictomboy.py 2010-08-13 23:44:44 +0000 |
147 | @@ -0,0 +1,583 @@ |
148 | +# -*- coding: utf-8 -*- |
149 | +# ----------------------------------------------------------------------------- |
150 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
151 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
152 | +# |
153 | +# This program is free software: you can redistribute it and/or modify it under |
154 | +# the terms of the GNU General Public License as published by the Free Software |
155 | +# Foundation, either version 3 of the License, or (at your option) any later |
156 | +# version. |
157 | +# |
158 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
159 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
160 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
161 | +# details. |
162 | +# |
163 | +# You should have received a copy of the GNU General Public License along with |
164 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
165 | +# ----------------------------------------------------------------------------- |
166 | + |
167 | +''' |
168 | +Contains the Backend class for both Tomboy and Gnote |
169 | +''' |
170 | +#Note: To introspect tomboy, execute: |
171 | +# qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl |
172 | + |
173 | +import os |
174 | +import re |
175 | +import threading |
176 | +import uuid |
177 | +import dbus |
178 | +import datetime |
179 | + |
180 | +from GTG.tools.testingmode import TestingMode |
181 | +from GTG.tools.borg import Borg |
182 | +from GTG.backends.genericbackend import GenericBackend |
183 | +from GTG.backends.backendsignals import BackendSignals |
184 | +from GTG.backends.syncengine import SyncEngine, SyncMeme |
185 | +from GTG.tools.logger import Log |
186 | +from GTG.tools.watchdog import Watchdog |
187 | +from GTG.tools.interruptible import interruptible |
188 | + |
189 | + |
190 | + |
191 | +class GenericTomboy(GenericBackend): |
192 | + '''Backend class for Tomboy/Gnote''' |
193 | + |
194 | + |
195 | +############################################################################### |
196 | +### Backend standard methods ################################################## |
197 | +############################################################################### |
198 | + |
199 | + def __init__(self, parameters): |
200 | + """ |
201 | + See GenericBackend for an explanation of this function. |
202 | + """ |
203 | + super(GenericTomboy, self).__init__(parameters) |
204 | + #loading the saved state of the synchronization, if any |
205 | + self.data_path = os.path.join('backends/tomboy/', \ |
206 | + "sync_engine-" + self.get_id()) |
207 | + self.sync_engine = self._load_pickled_file(self.data_path, \ |
208 | + SyncEngine()) |
209 | + #if the backend is being tested, we connect to a different DBus |
210 | + # interface to avoid clashing with a running instance of Tomboy |
211 | + if TestingMode().get_testing_mode(): |
212 | + #just used for testing purposes |
213 | + self.BUS_ADDRESS = \ |
214 | + self._parameters["use this fake connection instead"] |
215 | + else: |
216 | + self.BUS_ADDRESS = self._BUS_ADDRESS |
217 | + #we let some time pass before considering a tomboy task for importing, |
218 | + # as the user may still be editing it. Here, we store the Timer objects |
219 | + # that will execute after some time after each tomboy signal. |
220 | + #NOTE: I'm not sure if this is the case anymore (but it shouldn't hurt |
221 | + # anyway). (invernizzi) |
222 | + self._tomboy_setting_timers = {} |
223 | + |
224 | + def initialize(self): |
225 | + ''' |
226 | + See GenericBackend for an explanation of this function. |
227 | + Connects to the session bus and sets the callbacks for bus signals |
228 | + ''' |
229 | + super(GenericTomboy, self).initialize() |
230 | + with self.DbusWatchdog(self): |
231 | + bus = dbus.SessionBus() |
232 | + bus.add_signal_receiver(self.on_note_saved, |
233 | + dbus_interface = self.BUS_ADDRESS[2], |
234 | + signal_name = "NoteSaved") |
235 | + bus.add_signal_receiver(self.on_note_deleted, |
236 | + dbus_interface = self.BUS_ADDRESS[2], |
237 | + signal_name = "NoteDeleted") |
238 | + |
239 | + @interruptible |
240 | + def start_get_tasks(self): |
241 | + ''' |
242 | + See GenericBackend for an explanation of this function. |
243 | + Gets all the notes from Tomboy and sees if they must be added in GTG |
244 | + (and, if so, it adds them). |
245 | + ''' |
246 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
247 | + with self.DbusWatchdog(self): |
248 | + tomboy_notes = [str(note_id) for note_id in \ |
249 | + tomboy.ListAllNotes()] |
250 | + #adding the new ones |
251 | + for note in tomboy_notes: |
252 | + self.cancellation_point() |
253 | + self._process_tomboy_note(note) |
254 | + #checking if some notes have been deleted while GTG was not running |
255 | + stored_notes_ids = self.sync_engine.get_all_remote() |
256 | + for note in set(stored_notes_ids).difference(set(tomboy_notes)): |
257 | + self.on_note_deleted(note, None) |
258 | + |
259 | + def save_state(self): |
260 | + '''Saves the state of the synchronization''' |
261 | + self._store_pickled_file(self.data_path, self.sync_engine) |
262 | + |
263 | + def quit(self, disable = False): |
264 | + ''' |
265 | + See GenericBackend for an explanation of this function. |
266 | + ''' |
267 | + def quit_thread(): |
268 | + while True: |
269 | + try: |
270 | + [key, timer] = \ |
271 | + self._tomboy_setting_timers.iteritems().next() |
272 | + except StopIteration: |
273 | + break |
274 | + timer.cancel() |
275 | + del self._tomboy_setting_timers[key] |
276 | + threading.Thread(target = quit_thread).start() |
277 | + super(GenericTomboy, self).quit(disable) |
278 | + |
279 | +############################################################################### |
280 | +### Something got removed ##################################################### |
281 | +############################################################################### |
282 | + |
283 | + @interruptible |
284 | + def on_note_deleted(self, note, something): |
285 | + ''' |
286 | + Callback, executed when a tomboy note is deleted. |
287 | + Deletes the related GTG task. |
288 | + |
289 | + @param note: the id of the Tomboy note |
290 | + @param something: not used, here for signal callback compatibility |
291 | + ''' |
292 | + note = str(note) |
293 | + with self.datastore.get_backend_mutex(): |
294 | + self.cancellation_point() |
295 | + try: |
296 | + tid = self.sync_engine.get_local_id(note) |
297 | + except KeyError: |
298 | + return |
299 | + if self.datastore.has_task(tid): |
300 | + self.datastore.request_task_deletion(tid) |
301 | + self.break_relationship(remote_id = note) |
302 | + |
303 | + @interruptible |
304 | + def remove_task(self, tid): |
305 | + ''' |
306 | + See GenericBackend for an explanation of this function. |
307 | + ''' |
308 | + with self.datastore.get_backend_mutex(): |
309 | + self.cancellation_point() |
310 | + try: |
311 | + note = self.sync_engine.get_remote_id(tid) |
312 | + except KeyError: |
313 | + return |
314 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
315 | + with self.DbusWatchdog(self): |
316 | + if tomboy.NoteExists(note): |
317 | + tomboy.DeleteNote(note) |
318 | + self.break_relationship(local_id = tid) |
319 | + |
320 | + def _exec_lost_syncability(self, tid, note): |
321 | + ''' |
322 | + Executed when a relationship between tasks loses its syncability |
323 | + property. See SyncEngine for an explanation of that. |
324 | + This function finds out which object (task/note) is the original one |
325 | + and which is the copy, and deletes the copy. |
326 | + |
327 | + @param tid: a GTG task tid |
328 | + #param note: a tomboy note id |
329 | + ''' |
330 | + self.cancellation_point() |
331 | + meme = self.sync_engine.get_meme_from_remote_id(note) |
332 | + #First of all, the relationship is lost |
333 | + self.sync_engine.break_relationship(remote_id = note) |
334 | + if meme.get_origin() == "GTG": |
335 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
336 | + with self.DbusWatchdog(self): |
337 | + tomboy.DeleteNote(note) |
338 | + else: |
339 | + self.datastore.request_task_deletion(tid) |
340 | + |
341 | +############################################################################### |
342 | +### Process tasks ############################################################# |
343 | +############################################################################### |
344 | + |
345 | + def _process_tomboy_note(self, note): |
346 | + ''' |
347 | + Given a tomboy note, finds out if it must be synced to a GTG note and, |
348 | + if so, it carries out the synchronization (by creating or updating a GTG |
349 | + task, or deleting itself if the related task has been deleted) |
350 | + |
351 | + @param note: a Tomboy note id |
352 | + ''' |
353 | + with self.datastore.get_backend_mutex(): |
354 | + self.cancellation_point() |
355 | + is_syncable = self._tomboy_note_is_syncable(note) |
356 | + with self.DbusWatchdog(self): |
357 | + action, tid = self.sync_engine.analyze_remote_id(note, \ |
358 | + self.datastore.has_task, \ |
359 | + self._tomboy_note_exists, is_syncable) |
360 | + Log.debug("processing tomboy (%s, %s)" % (action, is_syncable)) |
361 | + |
362 | + if action == SyncEngine.ADD: |
363 | + tid = str(uuid.uuid4()) |
364 | + task = self.datastore.task_factory(tid) |
365 | + self._populate_task(task, note) |
366 | + self.record_relationship(local_id = tid,\ |
367 | + remote_id = note, \ |
368 | + meme = SyncMeme(task.get_modified(), |
369 | + self.get_modified_for_note(note), |
370 | + self.get_id())) |
371 | + self.datastore.push_task(task) |
372 | + |
373 | + elif action == SyncEngine.UPDATE: |
374 | + task = self.datastore.get_task(tid) |
375 | + meme = self.sync_engine.get_meme_from_remote_id(note) |
376 | + newest = meme.which_is_newest(task.get_modified(), |
377 | + self.get_modified_for_note(note)) |
378 | + if newest == "remote": |
379 | + self._populate_task(task, note) |
380 | + meme.set_local_last_modified(task.get_modified()) |
381 | + meme.set_remote_last_modified(\ |
382 | + self.get_modified_for_note(note)) |
383 | + self.save_state() |
384 | + |
385 | + elif action == SyncEngine.REMOVE: |
386 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
387 | + with self.DbusWatchdog(self): |
388 | + tomboy.DeleteNote(note) |
389 | + try: |
390 | + self.sync_engine.break_relationship(remote_id = note) |
391 | + except KeyError: |
392 | + pass |
393 | + |
394 | + elif action == SyncEngine.LOST_SYNCABILITY: |
395 | + self._exec_lost_syncability(tid, note) |
396 | + |
397 | + @interruptible |
398 | + def set_task(self, task): |
399 | + ''' |
400 | + See GenericBackend for an explanation of this function. |
401 | + ''' |
402 | + self.cancellation_point() |
403 | + is_syncable = self._gtg_task_is_syncable_per_attached_tags(task) |
404 | + tid = task.get_id() |
405 | + with self.datastore.get_backend_mutex(): |
406 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
407 | + with self.DbusWatchdog(self): |
408 | + action, note = self.sync_engine.analyze_local_id(tid, \ |
409 | + self.datastore.has_task, tomboy.NoteExists, \ |
410 | + is_syncable) |
411 | + Log.debug("processing gtg (%s, %d)" % (action, is_syncable)) |
412 | + |
413 | + if action == SyncEngine.ADD: |
414 | + #GTG allows multiple tasks with the same name, |
415 | + #Tomboy doesn't. we need to handle the renaming |
416 | + #manually |
417 | + title = task.get_title() |
418 | + duplicate_counter = 1 |
419 | + with self.DbusWatchdog(self): |
420 | + note = str(tomboy.CreateNamedNote(title)) |
421 | + while note == "": |
422 | + duplicate_counter += 1 |
423 | + note = tomboy.CreateNamedNote(title + "(%d)" % |
424 | + duplicate_counter) |
425 | + if duplicate_counter != 1: |
426 | + #if we needed to rename, we have to rename also |
427 | + # the gtg task |
428 | + task.set_title(title + " (%d)" % duplicate_counter) |
429 | + |
430 | + self._populate_note(note, task) |
431 | + self.record_relationship( \ |
432 | + local_id = tid, remote_id = note, \ |
433 | + meme = SyncMeme(task.get_modified(), |
434 | + self.get_modified_for_note(note), |
435 | + "GTG")) |
436 | + |
437 | + elif action == SyncEngine.UPDATE: |
438 | + meme = self.sync_engine.get_meme_from_local_id(\ |
439 | + task.get_id()) |
440 | + newest = meme.which_is_newest(task.get_modified(), |
441 | + self.get_modified_for_note(note)) |
442 | + if newest == "local": |
443 | + self._populate_note(note, task) |
444 | + meme.set_local_last_modified(task.get_modified()) |
445 | + meme.set_remote_last_modified(\ |
446 | + self.get_modified_for_note(note)) |
447 | + self.save_state() |
448 | + |
449 | + elif action == SyncEngine.REMOVE: |
450 | + self.datastore.request_task_deletion(tid) |
451 | + try: |
452 | + self.sync_engine.break_relationship(local_id = tid) |
453 | + self.save_state() |
454 | + except KeyError: |
455 | + pass |
456 | + |
457 | + elif action == SyncEngine.LOST_SYNCABILITY: |
458 | + self._exec_lost_syncability(tid, note) |
459 | + |
460 | +############################################################################### |
461 | +### Helper methods ############################################################ |
462 | +############################################################################### |
463 | + |
464 | + @interruptible |
465 | + def on_note_saved(self, note): |
466 | + ''' |
467 | + Callback, executed when a tomboy note is saved by Tomboy itself. |
468 | + Updates the related GTG task (or creates one, if necessary). |
469 | + |
470 | + @param note: the id of the Tomboy note |
471 | + ''' |
472 | + note = str(note) |
473 | + self.cancellation_point() |
474 | + #NOTE: we let some seconds pass before executing the real callback, as |
475 | + # the editing of the Tomboy note may still be in progress |
476 | + @interruptible |
477 | + def _execute_on_note_saved(self, note): |
478 | + self.cancellation_point() |
479 | + try: |
480 | + del self._tomboy_setting_timers[note] |
481 | + except: |
482 | + pass |
483 | + self._process_tomboy_note(note) |
484 | + self.save_state() |
485 | + |
486 | + try: |
487 | + self._tomboy_setting_timers[note].cancel() |
488 | + except KeyError: |
489 | + pass |
490 | + finally: |
491 | + timer =threading.Timer(5, _execute_on_note_saved, |
492 | + args = (self, note)) |
493 | + self._tomboy_setting_timers[note] = timer |
494 | + timer.start() |
495 | + |
496 | + def _tomboy_note_is_syncable(self, note): |
497 | + ''' |
498 | + Returns True if this tomboy note should be synced into GTG tasks. |
499 | + |
500 | + @param note: the note id |
501 | + @returns Boolean |
502 | + ''' |
503 | + attached_tags = self.get_attached_tags() |
504 | + if GenericBackend.ALLTASKS_TAG in attached_tags: |
505 | + return True |
506 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
507 | + with self.DbusWatchdog(self): |
508 | + content = tomboy.GetNoteContents(note) |
509 | + syncable = False |
510 | + for tag in attached_tags: |
511 | + try: |
512 | + content.index(tag) |
513 | + syncable = True |
514 | + break |
515 | + except ValueError: |
516 | + pass |
517 | + return syncable |
518 | + |
519 | + def _tomboy_note_exists(self, note): |
520 | + ''' |
521 | + Returns True if a tomboy note exists with the given id. |
522 | + |
523 | + @param note: the note id |
524 | + @returns Boolean |
525 | + ''' |
526 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
527 | + with self.DbusWatchdog(self): |
528 | + return tomboy.NoteExists(note) |
529 | + |
530 | + def get_modified_for_note(self, note): |
531 | + ''' |
532 | + Returns the modification time for the given note id. |
533 | + |
534 | + @param note: the note id |
535 | + @returns datetime.datetime |
536 | + ''' |
537 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
538 | + with self.DbusWatchdog(self): |
539 | + return datetime.datetime.fromtimestamp( \ |
540 | + tomboy.GetNoteChangeDate(note)) |
541 | + |
542 | + def _tomboy_split_title_and_text(self, content): |
543 | + ''' |
544 | + Tomboy does not have a "getTitle" and "getText" functions to get the |
545 | + title and the text of a note separately. Instead, it has a getContent |
546 | + function, that returns both of them. |
547 | + This function splits up the output of getContent into a title string and |
548 | + a text string. |
549 | + |
550 | + @param content: a string, the result of a getContent call |
551 | + @returns list: a list composed by [title, text] |
552 | + ''' |
553 | + try: |
554 | + end_of_title = content.index('\n') |
555 | + except ValueError: |
556 | + return content, "" |
557 | + title = content[: end_of_title] |
558 | + if len(content) > end_of_title: |
559 | + return title, content[end_of_title +1 :] |
560 | + else: |
561 | + return title, "" |
562 | + |
563 | + def _populate_task(self, task, note): |
564 | + ''' |
565 | + Copies the content of a Tomboy note into a task. |
566 | + |
567 | + @param task: a GTG Task |
568 | + @param note: a Tomboy note |
569 | + ''' |
570 | + #add tags objects (it's not enough to have @tag in the text to add a |
571 | + # tag |
572 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
573 | + with self.DbusWatchdog(self): |
574 | + content = tomboy.GetNoteContents(note) |
575 | + #update the tags list |
576 | + matches = re.finditer("(?<![^|\s])(@\w+)", content) |
577 | + new_tags_list = [content[g.start() : g.end()] for g in matches] |
578 | + for tag in task.get_tags_name(): |
579 | + try: |
580 | + new_tags_list.remove(tag) |
581 | + task.remove_tag(tag) |
582 | + except: |
583 | + task.add_tag(tag) |
584 | + for tag in new_tags_list: |
585 | + task.add_tag(tag) |
586 | + #extract title and text |
587 | + [title, text] = self._tomboy_split_title_and_text(content) |
588 | + task.set_title(title) |
589 | + task.set_text(text) |
590 | + task.add_remote_id(self.get_id(), note) |
591 | + |
592 | + def _populate_note(self, note, task): |
593 | + ''' |
594 | + Copies the content of a task into a Tomboy note. |
595 | + |
596 | + @param note: a Tomboy note |
597 | + @param task: a GTG Task |
598 | + ''' |
599 | + title = task.get_title() |
600 | + tested_title = title |
601 | + duplicate_counter = 1 |
602 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
603 | + with self.DbusWatchdog(self): |
604 | + tomboy.SetNoteContents(note, title + '\n' + \ |
605 | + task.get_excerpt(strip_tags = False)) |
606 | + |
607 | + def break_relationship(self, *args, **kwargs): |
608 | + ''' |
609 | + Proxy method for SyncEngine.break_relationship, which also saves the |
610 | + state of the synchronization. |
611 | + ''' |
612 | + try: |
613 | + self.sync_engine.break_relationship(*args, **kwargs) |
614 | + #we try to save the state at each change in the sync_engine: |
615 | + #it's slower, but it should avoid widespread task |
616 | + #duplication |
617 | + self.save_state() |
618 | + except KeyError: |
619 | + pass |
620 | + |
621 | + def record_relationship(self, *args, **kwargs): |
622 | + ''' |
623 | + Proxy method for SyncEngine.break_relationship, which also saves the |
624 | + state of the synchronization. |
625 | + ''' |
626 | + self.sync_engine.record_relationship(*args, **kwargs) |
627 | + #we try to save the state at each change in the sync_engine: |
628 | + #it's slower, but it should avoid widespread task |
629 | + #duplication |
630 | + self.save_state() |
631 | + |
632 | +############################################################################### |
633 | +### Connection handling ####################################################### |
634 | +############################################################################### |
635 | + |
636 | + |
637 | + |
638 | + class TomboyConnection(Borg): |
639 | + ''' |
640 | + TomboyConnection creates a connection to TOMBOY via DBUS and |
641 | + handles all the possible exceptions. |
642 | + It is a class that can be used with a with statement. |
643 | + Example: |
644 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
645 | + #do something |
646 | + ''' |
647 | + |
648 | + |
649 | + def __init__(self, backend, bus_name, bus_path, bus_interface): |
650 | + ''' |
651 | + Sees if a TomboyConnection object already exists. If so, since we |
652 | + are inheriting from a Borg object, the initialization already took |
653 | + place. |
654 | + If not, it tries to connect to Tomboy via Dbus. If the connection |
655 | + is not possible, the user is notified about it. |
656 | + |
657 | + @param backend: a reference to a Backend |
658 | + @param bus_name: the DBUS address of Tomboy |
659 | + @param bus_path: the DBUS path of Tomboy RemoteControl |
660 | + @param bus_interface: the DBUS address of Tomboy RemoteControl |
661 | + ''' |
662 | + super(GenericTomboy.TomboyConnection, self).__init__() |
663 | + if hasattr(self, "tomboy_connection_is_ok") and \ |
664 | + self.tomboy_connection_is_ok: |
665 | + return |
666 | + self.backend = backend |
667 | + with GenericTomboy.DbusWatchdog(backend): |
668 | + bus = dbus.SessionBus() |
669 | + obj = bus.get_object(bus_name, bus_path) |
670 | + self.tomboy = dbus.Interface(obj, bus_interface) |
671 | + self.tomboy_connection_is_ok = True |
672 | + |
673 | + def __enter__(self): |
674 | + ''' |
675 | + Returns the Tomboy connection |
676 | + |
677 | + @returns dbus.Interface |
678 | + ''' |
679 | + return self.tomboy |
680 | + |
681 | + def __exit__(self, exception_type, value, traceback): |
682 | + ''' |
683 | + Checks the state of the connection. |
684 | + If something went wrong for the connection, notifies the user. |
685 | + |
686 | + @param exception_type: the type of exception that occurred, or |
687 | + None |
688 | + @param value: the instance of the exception occurred, or None |
689 | + @param traceback: the traceback of the error |
690 | + @returns: False if some exception must be re-raised. |
691 | + ''' |
692 | + if isinstance(value, dbus.DBusException): |
693 | + self.tomboy_connection_is_ok = False |
694 | + self.backend.quit(disable = True) |
695 | + BackendSignals().backend_failed(self.backend.get_id(), \ |
696 | + BackendSignals.ERRNO_DBUS) |
697 | + else: |
698 | + return False |
699 | + return True |
700 | + |
701 | + |
702 | + |
703 | + class DbusWatchdog(Watchdog): |
704 | + ''' |
705 | + A simple watchdog to detect stale dbus connections |
706 | + ''' |
707 | + |
708 | + |
709 | + def __init__(self, backend): |
710 | + ''' |
711 | + Simple constructor, which sets _when_taking_too_long as the function |
712 | + to run when the connection is taking too long. |
713 | + |
714 | + @param backend: a Backend object |
715 | + ''' |
716 | + self.backend = backend |
717 | + super(GenericTomboy.DbusWatchdog, self).__init__(3, \ |
718 | + self._when_taking_too_long) |
719 | + |
720 | + def _when_taking_too_long(self): |
721 | + ''' |
722 | + Function that is executed when the Dbus connection seems to be |
723 | + hanging. It disables the backend and signals the error to the user. |
724 | + ''' |
725 | + Log.error("Dbus connection is taking too long for the Tomboy/Gnote" |
726 | + "backend!") |
727 | + self.backend.quit(disable = True) |
728 | + BackendSignals().backend_failed(self.backend.get_id(), \ |
729 | + BackendSignals.ERRNO_DBUS) |
730 | + |
731 | |
732 | === modified file 'GTG/gtk/browser/browser.py' |
733 | --- GTG/gtk/browser/browser.py 2010-08-10 17:30:24 +0000 |
734 | +++ GTG/gtk/browser/browser.py 2010-08-13 23:44:44 +0000 |
735 | @@ -953,7 +953,9 @@ |
736 | text = \ |
737 | text.replace("%s%s:%s" % (spaces, attribute, args), "") |
738 | # Create the new task |
739 | - task = self.req.new_task(tags=[t.get_name() for t in tags], newtask=True) |
740 | + task = self.req.new_task( newtask=True) |
741 | + for tag in tags: |
742 | + task.add_tag(tag.get_name()) |
743 | if text != "": |
744 | task.set_title(text.strip()) |
745 | task.set_to_keep() |
746 | |
747 | === added file 'data/icons/hicolor/scalable/apps/backend_gnote.png' |
748 | Binary files data/icons/hicolor/scalable/apps/backend_gnote.png 1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_gnote.png 2010-08-13 23:44:44 +0000 differ |
749 | === added file 'data/icons/hicolor/scalable/apps/backend_tomboy.png' |
750 | Binary files data/icons/hicolor/scalable/apps/backend_tomboy.png 1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_tomboy.png 2010-08-13 23:44:44 +0000 differ |
Not touching anything in GTG so, obviously, you can merge it. Very good work !