← Back to team overview

gtg team mailing list archive

[Merge] lp:~gtg-user/gtg/tomboy-backend into lp:gtg

 

Luca Invernizzi has proposed merging lp:~gtg-user/gtg/tomboy-backend into lp:gtg with lp:~gtg-user/gtg/multibackends-halfgsoc_merge as a prerequisite.

Requested reviews:
  Lionel Dricot (ploum)
  Gtg developers (gtg)


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-.

-- 
https://code.launchpad.net/~gtg-user/gtg/tomboy-backend/+merge/32646
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/tomboy-backend into lp:gtg.
=== modified file 'CHANGELOG'
--- CHANGELOG	2010-08-04 00:30:22 +0000
+++ CHANGELOG	2010-08-13 23:45:17 +0000
@@ -4,6 +4,7 @@
     * Fixed bug with data consistency #579189, by Marko Kevac
     * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
     * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
+    * New Tomboy/Gnote backend, by Luca Invernizzi
 
 2010-03-01 Getting Things GNOME! 0.2.2
     * Autostart on login, by Luca Invernizzi

=== added file 'GTG/backends/backend_gnote.py'
--- GTG/backends/backend_gnote.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_gnote.py	2010-08-13 23:45:17 +0000
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+The gnote backend. The actual backend is all in GenericTomboy, since it's
+shared with the tomboy backend.
+'''
+#To introspect tomboy: qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl
+
+from GTG.backends.genericbackend import GenericBackend
+from GTG                         import _
+from GTG.backends.generictomboy  import GenericTomboy
+
+
+
+class Backend(GenericTomboy):
+    '''
+    A simple class that adds some description to the GenericTomboy class.
+    It's done this way since Tomboy and Gnote backends have different
+    descriptions and Dbus addresses but the same backend behind them.
+    '''
+    
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_gnote", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Gnote"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("This backend can synchronize all or part of your Gnote"
+              " notes in GTG. If you decide it would be handy to"
+              " have one of your notes in your TODO list, just tag it "
+              "with the tag you have chosen (you'll configure it later"
+              "), and it will appear in GTG."),\
+        }
+
+    _static_parameters = { \
+        GenericBackend.KEY_ATTACHED_TAGS: {\
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
+            GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Gnote"]}, \
+        }
+
+    _BUS_ADDRESS = ("org.gnome.Gnote",
+                     "/org/gnome/Gnote/RemoteControl",
+                     "org.gnome.Gnote.RemoteControl")

=== added file 'GTG/backends/backend_tomboy.py'
--- GTG/backends/backend_tomboy.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_tomboy.py	2010-08-13 23:45:17 +0000
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+The tomboy backend. The actual backend is all in GenericTomboy, since it's
+shared with the Gnote backend.
+'''
+
+from GTG.backends.genericbackend import GenericBackend
+from GTG                         import _
+from GTG.backends.generictomboy  import GenericTomboy
+
+
+
+class Backend(GenericTomboy):
+    '''
+    A simple class that adds some description to the GenericTomboy class.
+    It's done this way since Tomboy and Gnote backends have different
+    descriptions and Dbus addresses but the same backend behind them.
+    '''
+    
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_tomboy", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Tomboy"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("This backend can synchronize all or part of your Tomboy"
+              " notes in GTG. If you decide it would be handy to"
+              " have one of your notes in your TODO list, just tag it "
+              "with the tag you have chosen (you'll configure it later"
+              "), and it will appear in GTG."),\
+        }
+
+    _static_parameters = { \
+        GenericBackend.KEY_ATTACHED_TAGS: {\
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
+            GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Tomboy"]}, \
+        }
+
+    _BUS_ADDRESS = ("org.gnome.Tomboy",
+                     "/org/gnome/Tomboy/RemoteControl",
+                     "org.gnome.Tomboy.RemoteControl")

=== added file 'GTG/backends/generictomboy.py'
--- GTG/backends/generictomboy.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/generictomboy.py	2010-08-13 23:45:17 +0000
@@ -0,0 +1,583 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Contains the Backend class for both Tomboy and Gnote
+'''
+#Note: To introspect tomboy, execute:
+#    qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl
+
+import os
+import re
+import threading
+import uuid
+import dbus
+import datetime
+
+from GTG.tools.testingmode       import TestingMode
+from GTG.tools.borg              import Borg
+from GTG.backends.genericbackend import GenericBackend
+from GTG.backends.backendsignals import BackendSignals
+from GTG.backends.syncengine     import SyncEngine, SyncMeme
+from GTG.tools.logger            import Log
+from GTG.tools.watchdog          import Watchdog
+from GTG.tools.interruptible     import interruptible
+
+
+
+class GenericTomboy(GenericBackend):
+    '''Backend class for Tomboy/Gnote'''
+    
+
+###############################################################################
+### Backend standard methods ##################################################
+###############################################################################
+
+    def __init__(self, parameters):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        super(GenericTomboy, self).__init__(parameters)
+        #loading the saved state of the synchronization, if any
+        self.data_path = os.path.join('backends/tomboy/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.data_path, \
+                                                   SyncEngine())
+        #if the backend is being tested, we connect to a different DBus
+        # interface to avoid clashing with a running instance of Tomboy
+        if TestingMode().get_testing_mode():
+            #just used for testing purposes
+            self.BUS_ADDRESS = \
+                    self._parameters["use this fake connection instead"]
+        else:
+            self.BUS_ADDRESS = self._BUS_ADDRESS
+        #we let some time pass before considering a tomboy task for importing,
+        # as the user may still be editing it. Here, we store the Timer objects
+        # that will execute after some time after each tomboy signal.
+        #NOTE: I'm not sure if this is the case anymore (but it shouldn't hurt
+        #      anyway). (invernizzi)
+        self._tomboy_setting_timers = {}
+        
+    def initialize(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        Connects to the session bus and sets the callbacks for bus signals
+        '''
+        super(GenericTomboy, self).initialize()
+        with self.DbusWatchdog(self):
+            bus = dbus.SessionBus()
+            bus.add_signal_receiver(self.on_note_saved,
+                                    dbus_interface = self.BUS_ADDRESS[2],
+                                    signal_name    = "NoteSaved")
+            bus.add_signal_receiver(self.on_note_deleted,
+                                    dbus_interface = self.BUS_ADDRESS[2],
+                                    signal_name    = "NoteDeleted")
+
+    @interruptible
+    def start_get_tasks(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        Gets all the notes from Tomboy and sees if they must be added in GTG
+        (and, if so, it adds them).
+        '''
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            with self.DbusWatchdog(self):
+                tomboy_notes = [str(note_id) for note_id in \
+                                tomboy.ListAllNotes()]
+        #adding the new ones
+        for note in tomboy_notes:
+            self.cancellation_point()
+            self._process_tomboy_note(note)
+        #checking if some notes have been deleted while GTG was not running
+        stored_notes_ids = self.sync_engine.get_all_remote()
+        for note in set(stored_notes_ids).difference(set(tomboy_notes)):
+            self.on_note_deleted(note, None)
+
+    def save_state(self):
+        '''Saves the state of the synchronization'''
+        self._store_pickled_file(self.data_path, self.sync_engine)
+
+    def quit(self, disable = False):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        def quit_thread():
+            while True:
+                try:
+                    [key, timer] = \
+                        self._tomboy_setting_timers.iteritems().next()
+                except StopIteration:
+                    break
+                timer.cancel()
+                del self._tomboy_setting_timers[key]
+        threading.Thread(target = quit_thread).start()
+        super(GenericTomboy, self).quit(disable)
+
+###############################################################################
+### Something got removed #####################################################
+###############################################################################
+
+    @interruptible
+    def on_note_deleted(self, note, something):
+        '''
+        Callback, executed when a tomboy note is deleted.
+        Deletes the related GTG task.
+
+        @param note: the id of the Tomboy note
+        @param something: not used, here for signal callback compatibility
+        '''
+        note = str(note)
+        with self.datastore.get_backend_mutex():
+            self.cancellation_point()
+            try:
+                tid = self.sync_engine.get_local_id(note)
+            except KeyError:
+                return
+            if self.datastore.has_task(tid):
+                self.datastore.request_task_deletion(tid)
+                self.break_relationship(remote_id = note)
+
+    @interruptible
+    def remove_task(self, tid):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        with self.datastore.get_backend_mutex():
+            self.cancellation_point()
+            try:
+                note = self.sync_engine.get_remote_id(tid)
+            except KeyError:
+                return
+            with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+                with self.DbusWatchdog(self):
+                    if tomboy.NoteExists(note):
+                        tomboy.DeleteNote(note)
+                        self.break_relationship(local_id = tid)
+
+    def _exec_lost_syncability(self, tid, note):
+        '''
+        Executed when a relationship between tasks loses its syncability
+        property. See SyncEngine for an explanation of that.
+        This function finds out which object (task/note) is the original one
+        and which is the copy, and deletes the copy.
+
+        @param tid: a GTG task tid
+        #param note: a tomboy note id
+        '''
+        self.cancellation_point()
+        meme = self.sync_engine.get_meme_from_remote_id(note)
+        #First of all, the relationship is lost
+        self.sync_engine.break_relationship(remote_id = note)
+        if meme.get_origin() == "GTG":
+            with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+                with self.DbusWatchdog(self):
+                    tomboy.DeleteNote(note)
+        else:
+            self.datastore.request_task_deletion(tid)
+
+###############################################################################
+### Process tasks #############################################################
+###############################################################################
+
+    def _process_tomboy_note(self, note):
+        '''
+        Given a tomboy note, finds out if it must be synced to a GTG note and, 
+        if so, it carries out the synchronization (by creating or updating a GTG
+        task, or deleting itself if the related task has been deleted)
+
+        @param note: a Tomboy note id
+        '''
+        with self.datastore.get_backend_mutex():
+            self.cancellation_point()
+            is_syncable = self._tomboy_note_is_syncable(note)
+            with self.DbusWatchdog(self):
+                action, tid = self.sync_engine.analyze_remote_id(note, \
+                         self.datastore.has_task, \
+                         self._tomboy_note_exists, is_syncable)
+            Log.debug("processing tomboy (%s, %s)" % (action, is_syncable))
+
+            if action == SyncEngine.ADD:
+                tid = str(uuid.uuid4())
+                task = self.datastore.task_factory(tid)
+                self._populate_task(task, note)
+                self.record_relationship(local_id = tid,\
+                            remote_id = note, \
+                            meme = SyncMeme(task.get_modified(),
+                                            self.get_modified_for_note(note),
+                                            self.get_id()))
+                self.datastore.push_task(task)
+
+            elif action == SyncEngine.UPDATE:
+                task = self.datastore.get_task(tid)
+                meme = self.sync_engine.get_meme_from_remote_id(note)
+                newest = meme.which_is_newest(task.get_modified(),
+                                     self.get_modified_for_note(note))
+                if newest == "remote":
+                    self._populate_task(task, note)
+                    meme.set_local_last_modified(task.get_modified())
+                    meme.set_remote_last_modified(\
+                                        self.get_modified_for_note(note))
+                    self.save_state()
+
+            elif action == SyncEngine.REMOVE:
+                with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+                    with self.DbusWatchdog(self):
+                        tomboy.DeleteNote(note)
+                    try:
+                        self.sync_engine.break_relationship(remote_id = note)
+                    except KeyError:
+                        pass
+
+            elif action == SyncEngine.LOST_SYNCABILITY:
+                self._exec_lost_syncability(tid, note)
+
+    @interruptible
+    def set_task(self, task):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        self.cancellation_point()
+        is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
+        tid = task.get_id()
+        with self.datastore.get_backend_mutex():
+            with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+                with self.DbusWatchdog(self):
+                    action, note = self.sync_engine.analyze_local_id(tid, \
+                              self.datastore.has_task, tomboy.NoteExists, \
+                                                            is_syncable)
+                Log.debug("processing gtg (%s, %d)" % (action, is_syncable))
+
+                if action == SyncEngine.ADD:
+                    #GTG allows multiple tasks with the same name,
+                    #Tomboy doesn't. we need to handle the renaming
+                    #manually
+                    title = task.get_title()
+                    duplicate_counter = 1
+                    with self.DbusWatchdog(self):
+                        note = str(tomboy.CreateNamedNote(title))
+                        while note == "":
+                            duplicate_counter += 1
+                            note = tomboy.CreateNamedNote(title + "(%d)" %
+                                                      duplicate_counter)
+                    if duplicate_counter != 1:
+                        #if we needed to rename, we have to rename also
+                        # the gtg task
+                        task.set_title(title + " (%d)" % duplicate_counter)
+
+                    self._populate_note(note, task)
+                    self.record_relationship( \
+                        local_id = tid, remote_id = note, \
+                        meme = SyncMeme(task.get_modified(),
+                                        self.get_modified_for_note(note),
+                                        "GTG"))
+
+                elif action == SyncEngine.UPDATE:
+                    meme = self.sync_engine.get_meme_from_local_id(\
+                                                        task.get_id())
+                    newest = meme.which_is_newest(task.get_modified(),
+                                         self.get_modified_for_note(note))
+                    if newest == "local":
+                        self._populate_note(note, task)
+                        meme.set_local_last_modified(task.get_modified())
+                        meme.set_remote_last_modified(\
+                                            self.get_modified_for_note(note))
+                        self.save_state()
+
+                elif action == SyncEngine.REMOVE:
+                    self.datastore.request_task_deletion(tid)
+                    try:
+                        self.sync_engine.break_relationship(local_id = tid)
+                        self.save_state()
+                    except KeyError:
+                        pass
+
+                elif action == SyncEngine.LOST_SYNCABILITY:
+                    self._exec_lost_syncability(tid, note)
+
+###############################################################################
+### Helper methods ############################################################
+###############################################################################
+
+    @interruptible
+    def on_note_saved(self,  note):
+        '''
+        Callback, executed when a tomboy note is saved by Tomboy itself.
+        Updates the related GTG task (or creates one, if necessary).
+
+        @param note: the id of the Tomboy note
+        '''
+        note = str(note)
+        self.cancellation_point()
+        #NOTE: we let some seconds pass before executing the real callback, as
+        #      the editing of the Tomboy note may still be in progress
+        @interruptible
+        def _execute_on_note_saved(self, note):
+            self.cancellation_point()
+            try:
+                del self._tomboy_setting_timers[note]
+            except:
+                pass
+            self._process_tomboy_note(note)
+            self.save_state()
+
+        try:
+            self._tomboy_setting_timers[note].cancel()
+        except KeyError:
+            pass
+        finally:
+            timer =threading.Timer(5, _execute_on_note_saved,
+                                   args = (self, note))
+            self._tomboy_setting_timers[note] = timer
+            timer.start()
+
+    def _tomboy_note_is_syncable(self, note):
+        '''
+        Returns True if this tomboy note should be synced into GTG tasks.
+
+        @param note: the note id
+        @returns Boolean
+        '''
+        attached_tags = self.get_attached_tags()
+        if GenericBackend.ALLTASKS_TAG in attached_tags:
+            return True
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            with self.DbusWatchdog(self):
+                content = tomboy.GetNoteContents(note)
+            syncable = False
+            for tag in attached_tags:
+                try:
+                    content.index(tag)
+                    syncable = True
+                    break
+                except ValueError:
+                    pass
+            return syncable
+
+    def _tomboy_note_exists(self, note):
+        '''
+        Returns True if  a tomboy note exists with the given id.
+
+        @param note: the note id
+        @returns Boolean
+        '''
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            with self.DbusWatchdog(self):
+                return tomboy.NoteExists(note)
+
+    def get_modified_for_note(self, note):
+        '''
+        Returns the modification time for the given note id.
+
+        @param note: the note id
+        @returns datetime.datetime
+        '''
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            with self.DbusWatchdog(self):
+                return datetime.datetime.fromtimestamp( \
+                                tomboy.GetNoteChangeDate(note))
+
+    def _tomboy_split_title_and_text(self, content):
+        '''
+        Tomboy does not have a "getTitle" and "getText" functions to get the
+        title and the text of a note separately. Instead, it has a getContent
+        function, that returns both of them.
+        This function splits up the output of getContent into a title string and
+        a text string.
+
+        @param content: a string, the result of a getContent call
+        @returns list: a list composed by [title, text]
+        '''
+        try:
+            end_of_title = content.index('\n')
+        except ValueError:
+            return content, ""
+        title = content[: end_of_title]
+        if len(content) > end_of_title:
+            return title, content[end_of_title +1 :]
+        else:
+            return title, ""
+
+    def _populate_task(self, task, note):
+        '''
+        Copies the content of a Tomboy note into a task.
+
+        @param task: a GTG Task
+        @param note: a Tomboy note
+        '''
+        #add tags objects (it's not enough to have @tag in the text to add a
+        # tag
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            with self.DbusWatchdog(self):
+                content = tomboy.GetNoteContents(note)
+        #update the tags list
+        matches = re.finditer("(?<![^|\s])(@\w+)", content)
+        new_tags_list = [content[g.start() : g.end()] for g in matches]
+        for tag in task.get_tags_name():
+            try:
+                new_tags_list.remove(tag)
+                task.remove_tag(tag)
+            except:
+                task.add_tag(tag)
+        for tag in new_tags_list:
+            task.add_tag(tag)
+        #extract title and text
+        [title, text] = self._tomboy_split_title_and_text(content)
+        task.set_title(title)
+        task.set_text(text)
+        task.add_remote_id(self.get_id(), note)
+
+    def _populate_note(self, note, task):
+        '''
+        Copies the content of a task into a Tomboy note.
+
+        @param note: a Tomboy note
+        @param task: a GTG Task
+        '''
+        title = task.get_title()
+        tested_title = title
+        duplicate_counter = 1
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            with self.DbusWatchdog(self):
+                tomboy.SetNoteContents(note, title + '\n' + \
+                                       task.get_excerpt(strip_tags = False))
+
+    def break_relationship(self, *args, **kwargs):
+        '''
+        Proxy method for SyncEngine.break_relationship, which also saves the
+        state of the synchronization.
+        '''
+        try:
+            self.sync_engine.break_relationship(*args, **kwargs)
+            #we try to save the state at each change in the sync_engine:
+            #it's slower, but it should avoid widespread task
+            #duplication
+            self.save_state()
+        except KeyError:
+            pass
+
+    def record_relationship(self, *args, **kwargs):
+        '''
+        Proxy method for SyncEngine.break_relationship, which also saves the
+        state of the synchronization.
+        '''
+        self.sync_engine.record_relationship(*args, **kwargs)
+        #we try to save the state at each change in the sync_engine:
+        #it's slower, but it should avoid widespread task
+        #duplication
+        self.save_state()
+
+###############################################################################
+### Connection handling #######################################################
+###############################################################################
+
+
+
+    class TomboyConnection(Borg):
+        '''
+        TomboyConnection creates a connection to TOMBOY via DBUS and
+        handles all the possible exceptions.
+        It is a class that can be used with a with statement.
+        Example:
+        with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+            #do something
+        '''
+
+
+        def __init__(self, backend, bus_name, bus_path, bus_interface):
+            '''
+            Sees if a TomboyConnection object already exists. If so, since we
+            are inheriting from a Borg object, the initialization already took
+            place.
+            If not, it tries to connect to Tomboy via Dbus. If the connection
+            is not possible, the user is notified about it.
+
+            @param backend: a reference to a Backend
+            @param bus_name: the DBUS address of Tomboy
+            @param bus_path: the DBUS path of Tomboy RemoteControl
+            @param bus_interface: the DBUS address of Tomboy RemoteControl 
+            '''
+            super(GenericTomboy.TomboyConnection, self).__init__()
+            if hasattr(self, "tomboy_connection_is_ok") and \
+                                self.tomboy_connection_is_ok:
+                return
+            self.backend = backend
+            with GenericTomboy.DbusWatchdog(backend):
+                bus = dbus.SessionBus()
+                obj = bus.get_object(bus_name, bus_path)
+                self.tomboy = dbus.Interface(obj, bus_interface)
+            self.tomboy_connection_is_ok = True
+
+        def __enter__(self):
+            '''
+            Returns the Tomboy connection
+
+            @returns dbus.Interface
+            '''
+            return self.tomboy
+
+        def __exit__(self, exception_type, value, traceback):
+            '''
+            Checks the state of the connection.
+            If something went wrong for the connection, notifies the user.
+
+            @param exception_type: the type of exception that occurred, or
+                                   None
+            @param value: the instance of the exception occurred, or None
+            @param traceback: the traceback of the error
+            @returns: False if some exception must be re-raised.
+            '''
+            if isinstance(value, dbus.DBusException):
+                self.tomboy_connection_is_ok = False
+                self.backend.quit(disable = True)
+                BackendSignals().backend_failed(self.backend.get_id(), \
+                            BackendSignals.ERRNO_DBUS)
+            else:
+                return False
+            return True
+
+
+
+    class DbusWatchdog(Watchdog):
+        '''
+        A simple watchdog to detect stale dbus connections
+        '''
+
+
+        def __init__(self, backend):
+            '''
+            Simple constructor, which sets _when_taking_too_long as the function
+            to run when the connection is taking too long.
+
+            @param backend: a Backend object
+            '''
+            self.backend = backend
+            super(GenericTomboy.DbusWatchdog, self).__init__(3, \
+                                    self._when_taking_too_long)
+
+        def _when_taking_too_long(self):
+            '''
+            Function that is executed when the Dbus connection seems to be
+            hanging. It disables the backend and signals the error to the user.
+            '''
+            Log.error("Dbus connection is taking too long for the Tomboy/Gnote"
+                      "backend!")
+            self.backend.quit(disable = True)
+            BackendSignals().backend_failed(self.backend.get_id(), \
+                            BackendSignals.ERRNO_DBUS)
+

=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py	2010-08-10 17:30:24 +0000
+++ GTG/gtk/browser/browser.py	2010-08-13 23:45:17 +0000
@@ -953,7 +953,9 @@
                     text = \
                         text.replace("%s%s:%s" % (spaces, attribute, args), "")
             # Create the new task
-            task = self.req.new_task(tags=[t.get_name() for t in tags], newtask=True)
+            task = self.req.new_task( newtask=True)
+            for tag in tags:
+                task.add_tag(tag.get_name())
             if text != "":
                 task.set_title(text.strip())
                 task.set_to_keep()

=== added file 'data/icons/hicolor/scalable/apps/backend_gnote.png'
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:45:17 +0000 differ
=== added file 'data/icons/hicolor/scalable/apps/backend_tomboy.png'
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:45:17 +0000 differ

Follow ups