psa: remove old event feed items
[feedingit] / src / FeedingIt.py
index f7a45b3..f9ab155 100644 (file)
 # ============================================================================
 __appname__ = 'FeedingIt'
 __author__  = 'Yves Marcoz'
-__version__ = '0.8.0'
+__version__ = '0.9.1~woodchuck'
 __description__ = 'A simple RSS Reader for Maemo 5'
 # ============================================================================
 
 import gtk
-from pango import FontDescription
 import pango
 import hildon
 #import gtkhtml2
@@ -36,22 +35,23 @@ from webkit import WebView
 #    import gtkhtml2
 #    has_webkit=False
 from os.path import isfile, isdir, exists
-from os import mkdir, remove, stat
+from os import mkdir, remove, stat, environ
 import gobject
 from aboutdialog import HeAboutDialog
 from portrait import FremantleRotation
-from threading import Thread, activeCount
 from feedingitdbus import ServerObject
-from updatedbus import UpdateServerObject, get_lock
 from config import Config
 from cgi import escape
 import weakref
+import dbus
+import debugging
+import logging
+logger = logging.getLogger(__name__)
 
 from rss_sqlite import Listing
 from opml import GetOpmlData, ExportOpmlData
 
 import mainthread
-from jobmanager import JobManager
 
 from socket import setdefaulttimeout
 timeout = 5
@@ -125,6 +125,52 @@ FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
 ENTRY_TEMPLATE = entry_head
 ENTRY_TEMPLATE_UNREAD = entry_active_head
 
+notification_iface = None
+def notify(message):
+    def get_iface():
+        global notification_iface
+
+        bus = dbus.SessionBus()
+        proxy = bus.get_object('org.freedesktop.Notifications',
+                               '/org/freedesktop/Notifications')
+        notification_iface \
+            = dbus.Interface(proxy, 'org.freedesktop.Notifications')
+
+    def doit():
+        notification_iface.SystemNoteInfoprint("FeedingIt: " + message)
+
+    if notification_iface is None:
+        get_iface()
+
+    try:
+        doit()
+    except dbus.DBusException:
+        # Rebind the name and try again.
+        get_iface()
+        doit()
+
+def open_in_browser(link):
+    bus = dbus.SessionBus()
+    b_proxy = bus.get_object("com.nokia.osso_browser",
+                             "/com/nokia/osso_browser/request")
+    b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
+
+    notify("Opening %s" % link)
+
+    # We open the link asynchronously: if the web browser is not
+    # already running, this can take a while.
+    def error_handler():
+        """
+        Something went wrong opening the URL.
+        """
+        def e(exception):
+            notify("Error opening %s: %s" % (link, str(exception)))
+        return e
+
+    b_iface.open_new_window(link,
+                            reply_handler=lambda *args: None,
+                            error_handler=error_handler())
+
 ##
 # Removes HTML or XML character references and entities from a text string.
 #
@@ -175,7 +221,8 @@ class AddWidgetWizard(gtk.Dialog):
 
         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
         self.nameEntry.set_placeholder('Feed name')
-        if not titleIn == None:
+        # If titleIn matches urlIn, there is no title.
+        if not titleIn == None and titleIn != urlIn:
             self.nameEntry.set_text(titleIn)
             self.nameEntry.select_region(-1, -1)
 
@@ -279,10 +326,6 @@ class DownloadBar(gtk.ProgressBar):
         if hasattr (cls, 'class_init_done'):
             return
 
-        jm = JobManager ()
-        jm.stats_hook_register (cls.update_progress,
-                                run_in_main_thread=True)
-
         cls.downloadbars = []
         # Total number of jobs we are monitoring.
         cls.total = 0
@@ -293,6 +336,13 @@ class DownloadBar(gtk.ProgressBar):
 
         cls.class_init_done = True
 
+        bus = dbus.SessionBus()
+        bus.add_signal_receiver(handler_function=cls.update_progress,
+                                bus_name=None,
+                                signal_name='UpdateProgress',
+                                dbus_interface='org.marcoz.feedingit',
+                                path='/org/marcoz/feedingit/update')
+
     def __init__(self, parent):
         self.class_init ()
 
@@ -301,47 +351,46 @@ class DownloadBar(gtk.ProgressBar):
         self.downloadbars.append(weakref.ref (self))
         self.set_fraction(0)
         self.__class__.update_bars()
-        self.show_all()
 
     @classmethod
     def downloading(cls):
-        return hasattr (cls, 'jobs_at_start')
+        cls.class_init ()
+        return cls.done != cls.total
 
     @classmethod
-    def update_progress(cls, jm, old_stats, new_stats, updated_feed):
-        if not cls.downloading():
-            cls.jobs_at_start = old_stats['jobs-completed']
-
+    def update_progress(cls, percent_complete,
+                        completed, in_progress, queued,
+                        bytes_downloaded, bytes_updated, bytes_per_second,
+                        feed_updated):
         if not cls.downloadbars:
             return
 
-        if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
-            del cls.jobs_at_start
+        cls.total = completed + in_progress + queued
+        cls.done = completed
+        cls.progress = percent_complete / 100.
+        if cls.progress < 0: cls.progress = 0
+        if cls.progress > 1: cls.progress = 1
+
+        if feed_updated:
             for ref in cls.downloadbars:
                 bar = ref ()
                 if bar is None:
                     # The download bar disappeared.
                     cls.downloadbars.remove (ref)
                 else:
-                    bar.emit("download-done", None)
-            return
-
-        # This should never be called if new_stats['jobs'] is 0, but
-        # just in case...
-        cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
-        cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
-        cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
-                            + new_stats['jobs-queued']) / cls.total
-        cls.update_bars()
+                    bar.emit("download-done", feed_updated)
 
-        if updated_feed:
+        if in_progress == 0 and queued == 0:
             for ref in cls.downloadbars:
                 bar = ref ()
                 if bar is None:
                     # The download bar disappeared.
                     cls.downloadbars.remove (ref)
                 else:
-                    bar.emit("download-done", updated_feed)
+                    bar.emit("download-done", None)
+            return
+
+        cls.update_bars()
 
     @classmethod
     def update_bars(cls):
@@ -538,7 +587,7 @@ class SortList(hildon.StackableWindow):
                 ret = wizard.run()
                 if ret == 2:
                     (title, url, category) = wizard.getData()
-                    if (not title == '') and (not url == ''):
+                    if url != '':
                         self.listing.editFeed(key, title, url, category=category)
                         self.refreshList()
                 wizard.destroy()
@@ -559,97 +608,136 @@ class SortList(hildon.StackableWindow):
             ret = wizard.run()
             if ret == 2:
                 (title, url, category) = wizard.getData()
-                if (not title == '') and (not url == ''): 
+                if url:
                    self.listing.addFeed(title, url, category=category)
         wizard.destroy()
         self.refreshList()
                
 
 class DisplayArticle(hildon.StackableWindow):
-    def __init__(self, feed, id, key, config, listing):
+    """
+    A Widget for displaying an article.
+    """
+    def __init__(self, article_id, feed, feed_key, articles, config, listing):
+        """
+        article_id - The identifier of the article to load.
+
+        feed - The feed object containing the article (an
+        rss_sqlite:Feed object).
+
+        feed_key - The feed's identifier.
+
+        articles - A list of articles from the feed to display.
+        Needed for selecting the next/previous article (article_next).
+
+        config - A configuration object (config:Config).
+
+        listing - The listing object (rss_sqlite:Listing) that
+        contains the feed and article.
+        """
         hildon.StackableWindow.__init__(self)
-        #self.imageDownloader = ImageDownloader()
+
+        self.article_id = None
         self.feed = feed
-        self.listing=listing
-        self.key = key
-        self.id = id
-        #self.set_title(feed.getTitle(id))
-        self.set_title(self.listing.getFeedTitle(key))
+        self.feed_key = feed_key
+        self.articles = articles
         self.config = config
-        self.set_for_removal = False
-        
+        self.listing = listing
+
+        self.set_title(self.listing.getFeedTitle(feed_key))
+
         # Init the article display
-        #if self.config.getWebkitSupport():
         self.view = WebView()
-            #self.view.set_editable(False)
-        #else:
-        #    import gtkhtml2
-        #    self.view = gtkhtml2.View()
-        #    self.document = gtkhtml2.Document()
-        #    self.view.set_document(self.document)
-        #    self.document.connect("link_clicked", self._signal_link_clicked)
-        self.pannable_article = hildon.PannableArea()
-        self.pannable_article.add(self.view)
-        #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
-        #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
-
-        #if self.config.getWebkitSupport():
-        contentLink = self.feed.getContentLink(self.id)
-        self.feed.setEntryRead(self.id)
-        #if key=="ArchivedArticles":
-        self.loadedArticle = False
-        if contentLink.startswith("/home/user/"):
-            self.view.open("file://%s" % contentLink)
-            self.currentUrl = self.feed.getExternalLink(self.id)
-        else:
-            self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
-            self.currentUrl = "%s" % contentLink
+        self.view.set_zoom_level(float(config.getArtFontSize())/10.)
         self.view.connect("motion-notify-event", lambda w,ev: True)
         self.view.connect('load-started', self.load_started)
         self.view.connect('load-finished', self.load_finished)
+        self.view.connect('navigation-requested', self.navigation_requested)
+        self.view.connect("button_press_event", self.button_pressed)
+        self.gestureId = self.view.connect(
+            "button_release_event", self.button_released)
 
-        self.view.set_zoom_level(float(config.getArtFontSize())/10.)
-        
+        self.pannable_article = hildon.PannableArea()
+        self.pannable_article.add(self.view)
+
+        self.add(self.pannable_article)
+
+        self.pannable_article.show_all()
+
+        # Create the menu.
         menu = hildon.AppMenu()
-        # Create a button and add it to the menu
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Allow horizontal scrolling")
-        button.connect("clicked", self.horiz_scrolling_button)
-        menu.append(button)
-        
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Open in browser")
-        button.connect("clicked", self.open_in_browser)
-        menu.append(button)
-        
-        if key == "ArchivedArticles":
+
+        def menu_button(label, callback):
             button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-            button.set_label("Remove from archived articles")
-            button.connect("clicked", self.remove_archive_button)
+            button.set_label(label)
+            button.connect("clicked", callback)
+            menu.append(button)
+
+        menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
+        menu_button("Open in browser", self.open_in_browser)
+        if feed_key == "ArchivedArticles":
+            menu_button(
+                "Remove from archived articles", self.remove_archive_button)
         else:
-            button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-            button.set_label("Add to archived articles")
-            button.connect("clicked", self.archive_button)
-        menu.append(button)
+            menu_button("Add to archived articles", self.archive_button)
         
         self.set_app_menu(menu)
         menu.show_all()
         
-        self.add(self.pannable_article)
-        
-        self.pannable_article.show_all()
-
         self.destroyId = self.connect("destroy", self.destroyWindow)
-        
-        #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
-        ## Still using an old version of WebKit, so using navigation-requested signal
-        self.view.connect('navigation-requested', self.navigation_requested)
-        
-        self.view.connect("button_press_event", self.button_pressed)
-        self.gestureId = self.view.connect("button_release_event", self.button_released)
 
-    #def navigation_policy_decision(self, wv, fr, req, action, decision):
+        self.article_open(article_id)
+
+    def article_open(self, article_id):
+        """
+        Load the article with the specified id.
+        """
+        # If an article was open, close it.
+        if self.article_id is not None:
+            self.article_closed()
+
+        self.article_id = article_id
+        self.set_for_removal = False
+        self.loadedArticle = False
+        self.initial_article_load = True
+
+        contentLink = self.feed.getContentLink(self.article_id)
+        if contentLink.startswith("/home/user/"):
+            self.view.open("file://%s" % contentLink)
+            self.currentUrl = self.feed.getExternalLink(self.article_id)
+        else:
+            self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
+            self.currentUrl = str(contentLink)
+
+        self.feed.setEntryRead(self.article_id)
+
+    def article_closed(self):
+        """
+        The user has navigated away from the article.  Execute any
+        pending actions.
+        """
+        if self.set_for_removal:
+            self.emit("article-deleted", self.article_id)
+        else:
+            self.emit("article-closed", self.article_id)
+
+
     def navigation_requested(self, wv, fr, req):
+        """
+        http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
+
+        wv - a WebKitWebView
+        fr - a WebKitWebFrame
+        req - WebKitNetworkRequest
+        """
+        if self.initial_article_load:
+            # Always initially load an article in the internal
+            # browser.
+            self.initial_article_load = False
+            return False
+
+        # When following a link, only use the internal browser if so
+        # configured.  Otherwise, launch an external browser.
         if self.config.getOpenInExternalBrowser():
             self.open_in_browser(None, req.get_uri())
             return True
@@ -658,7 +746,7 @@ class DisplayArticle(hildon.StackableWindow):
 
     def load_started(self, *widget):
         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
-        
+
     def load_finished(self, *widget):
         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
         frame = self.view.get_main_frame()
@@ -668,7 +756,14 @@ class DisplayArticle(hildon.StackableWindow):
             self.loadedArticle = True
 
     def button_pressed(self, window, event):
-        #print event.x, event.y
+        """
+        The user pressed a "mouse button" (in our case, this means the
+        user likely started to drag with the finger).
+
+        We are only interested in whether the user performs a drag.
+        We record the starting position and when the user "releases
+        the button," we see how far the mouse moved.
+        """
         self.coords = (event.x, event.y)
         
     def button_released(self, window, event):
@@ -677,17 +772,38 @@ class DisplayArticle(hildon.StackableWindow):
         
         if (2*abs(y) < abs(x)):
             if (x > 15):
-                self.emit("article-previous", self.id)
+                self.article_next(forward=False)
             elif (x<-15):
-                self.emit("article-next", self.id)   
+                self.article_next(forward=True)
+
+            # We handled the event.  Don't propagate it further.
+            return True
+
+    def article_next(self, forward=True):
+        """
+        Advance to the next (or, if forward is false, the previous)
+        article.
+        """
+        first_id = None
+        id = self.article_id
+        i = 0
+        while True:
+            i += 1
+            id = self.feed.getNextId(id, forward)
+            if id == first_id:
+                # We looped.
+                break
+
+            if first_id is None:
+                first_id = id
+
+            if id in self.articles:
+                self.article_open(id)
+                break
 
     def destroyWindow(self, *args):
+        self.article_closed()
         self.disconnect(self.destroyId)
-        if self.set_for_removal:
-            self.emit("article-deleted", self.id)
-        else:
-            self.emit("article-closed", self.id)
-        #self.imageDownloader.stopAll()
         self.destroy()
         
     def horiz_scrolling_button(self, *widget):
@@ -696,32 +812,40 @@ class DisplayArticle(hildon.StackableWindow):
         
     def archive_button(self, *widget):
         # Call the listing.addArchivedArticle
-        self.listing.addArchivedArticle(self.key, self.id)
+        self.listing.addArchivedArticle(self.feed_key, self.article_id)
         
     def remove_archive_button(self, *widget):
         self.set_for_removal = True
 
     def open_in_browser(self, object, link=None):
-        import dbus
-        bus = dbus.SessionBus()
-        proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
-        iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
+        """
+        Open the specified link using the system's browser.  If not
+        link is specified, reopen the current page using the system's
+        browser.
+        """
         if link == None:
-            iface.open_new_window(self.currentUrl)
-        else:
-            iface.open_new_window(link)
+            link = self.currentUrl
+
+        open_in_browser(link)
 
 class DisplayFeed(hildon.StackableWindow):
-    def __init__(self, listing, feed, title, key, config, updateDbusHandler):
+    def __init__(self, listing, config):
         hildon.StackableWindow.__init__(self)
+        self.connect('configure-event', self.on_configure_event)
+        self.connect("delete_event", self.delete_window)
+        self.connect("destroy", self.destroyWindow)
+
         self.listing = listing
-        self.feed = feed
-        self.feedTitle = title
-        self.set_title(title)
-        self.key=key
-        self.current = list()
         self.config = config
-        self.updateDbusHandler = updateDbusHandler
+
+        # Articles to show.
+        #
+        # If hide read articles is set, this is set to the set of
+        # unread articles at the time that feed is loaded.  The last
+        # bit is important: when the user selects the next article,
+        # but then decides to move back, previous should select the
+        # just read article.
+        self.articles = list()
         
         self.downloadDialog = False
         
@@ -739,27 +863,50 @@ class DisplayFeed(hildon.StackableWindow):
         button.set_label("Mark all as read")
         button.connect("clicked", self.buttonReadAllClicked)
         menu.append(button)
-        
-        if key=="ArchivedArticles":
-            button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-            button.set_label("Delete read articles")
-            button.connect("clicked", self.buttonPurgeArticles)
-            menu.append(button)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Delete read articles")
+        button.connect("clicked", self.buttonPurgeArticles)
+        menu.append(button)
+        self.archived_article_buttons = [button]
         
         self.set_app_menu(menu)
         menu.show_all()
         
-        self.main_vbox = gtk.VBox(False, 0)
-        self.add(self.main_vbox)
+        self.feedItems = gtk.ListStore(str, str)
 
-        self.pannableFeed = None
-        self.displayFeed()
+        self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
+        self.feedList.set_model(self.feedItems)
+        self.feedList.set_rules_hint(True)
+        self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
+        self.feedList.set_hover_selection(False)
+        self.feedList.connect('hildon-row-tapped',
+                              self.on_feedList_row_activated)
+
+        self.markup_renderer = gtk.CellRendererText()
+        self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
+        self.markup_renderer.set_property('background', bg_color) #"#333333")
+        (width, height) = self.get_size()
+        self.markup_renderer.set_property('wrap-width', width-20)
+        self.markup_renderer.set_property('ypad', 8)
+        self.markup_renderer.set_property('xpad', 5)
+        markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
+                markup=FEED_COLUMN_MARKUP)
+        self.feedList.append_column(markup_column)
 
-        if DownloadBar.downloading ():
-            self.show_download_bar ()
+        vbox = gtk.VBox(False, 10)
+        vbox.pack_start(self.feedList)
         
-        self.connect('configure-event', self.on_configure_event)
-        self.connect("destroy", self.destroyWindow)
+        self.pannableFeed = hildon.PannableArea()
+        self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
+        self.pannableFeed.add_with_viewport(vbox)
+
+        self.main_vbox = gtk.VBox(False, 0)
+        self.main_vbox.pack_start(self.pannableFeed)
+
+        self.add(self.main_vbox)
+
+        self.main_vbox.show_all()
 
     def on_configure_event(self, window, event):
         if getattr(self, 'markup_renderer', None) is None:
@@ -774,61 +921,67 @@ class DisplayFeed(hildon.StackableWindow):
             self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
             it = self.feedItems.iter_next(it)
 
+    def delete_window(self, *args):
+        """
+        Prevent the window from being deleted.
+        """
+        self.hide()
+
+        try:
+            key = self.key
+        except AttributeError:
+            key = None
+
+        if key is not None:
+            self.listing.updateUnread(key)
+            self.emit("feed-closed", key)
+            self.key = None
+            self.feedItems.clear()
+
+        return True
+
     def destroyWindow(self, *args):
-        #self.feed.saveUnread(CONFIGDIR)
-        self.listing.updateUnread(self.key)
-        self.emit("feed-closed", self.key)
+        try:
+            key = self.key
+        except AttributeError:
+            key = None
+
         self.destroy()
-        #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
-        #self.listing.closeCurrentlyDisplayedFeed()
 
     def fix_title(self, title):
         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
 
-    def displayFeed(self):
-        if self.pannableFeed:
-            self.pannableFeed.destroy()
+    def displayFeed(self, key=None):
+        """
+        Select and display a feed.  If feed, title and key are None,
+        reloads the current feed.
+        """
+        if key:
+            try:
+                old_key = self.key
+            except AttributeError:
+                old_key = None
 
-        self.pannableFeed = hildon.PannableArea()
+            if old_key:
+                self.listing.updateUnread(self.key)
+                self.emit("feed-closed", self.key)
 
-        self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
+            self.feed = self.listing.getFeed(key)
+            self.feedTitle = self.listing.getFeedTitle(key)
+            self.key = key
 
-        self.feedItems = gtk.ListStore(str, str)
-        #self.feedList = gtk.TreeView(self.feedItems)
-        self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
-        self.feedList.set_rules_hint(True)
+            self.set_title(self.feedTitle)
 
-        selection = self.feedList.get_selection()
-        selection.set_mode(gtk.SELECTION_NONE)
-        #selection.connect("changed", lambda w: True)
-        
-        self.feedList.set_model(self.feedItems)
-        self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
-
-        
-        self.feedList.set_hover_selection(False)
-        #self.feedList.set_property('enable-grid-lines', True)
-        #self.feedList.set_property('hildon-mode', 1)
-        #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
-        
-        #self.feedList.connect('row-activated', self.on_feedList_row_activated)
+            selection = self.feedList.get_selection()
+            if selection is not None:
+                selection.set_mode(gtk.SELECTION_NONE)
 
-        vbox= gtk.VBox(False, 10)
-        vbox.pack_start(self.feedList)
+            for b in self.archived_article_buttons:
+                if key == "ArchivedArticles":
+                    b.show()
+                else:
+                    b.hide()
         
-        self.pannableFeed.add_with_viewport(vbox)
-
-        self.markup_renderer = gtk.CellRendererText()
-        self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
-        self.markup_renderer.set_property('background', bg_color) #"#333333")
-        (width, height) = self.get_size()
-        self.markup_renderer.set_property('wrap-width', width-20)
-        self.markup_renderer.set_property('ypad', 8)
-        self.markup_renderer.set_property('xpad', 5)
-        markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
-                markup=FEED_COLUMN_MARKUP)
-        self.feedList.append_column(markup_column)
-
         #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
         hideReadArticles = self.config.getHideReadArticles()
         if hideReadArticles:
@@ -836,38 +989,39 @@ class DisplayFeed(hildon.StackableWindow):
         else:
             articles = self.feed.getIds()
         
-        hasArticle = False
-        self.current = list()
+        self.articles[:] = []
+
+        self.feedItems.clear()
         for id in articles:
-            isRead = False
             try:
                 isRead = self.feed.isEntryRead(id)
-            except:
-                pass
+            except Exception:
+                isRead = False
             if not ( isRead and hideReadArticles ):
                 title = self.fix_title(self.feed.getTitle(id))
-                self.current.append(id)
+                self.articles.append(id)
                 if isRead:
                     markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
                 else:
                     markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
     
                 self.feedItems.append((markup, id))
-                hasArticle = True
-        if hasArticle:
-            self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
-        else:
+        if not articles:
+            # No articles.
             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
             self.feedItems.append((markup, ""))
 
-        self.main_vbox.pack_start(self.pannableFeed)
-        self.show_all()
+        self.show()
 
     def clear(self):
         self.pannableFeed.destroy()
         #self.remove(self.pannableFeed)
 
     def on_feedList_row_activated(self, treeview, path): #, column):
+        if not self.articles:
+            # There are not actually any articles.  Ignore.
+            return False
+
         selection = self.feedList.get_selection()
         selection.set_mode(gtk.SELECTION_SINGLE)
         self.feedList.get_selection().select_path(path)
@@ -879,8 +1033,7 @@ class DisplayFeed(hildon.StackableWindow):
         #return True
 
     def button_clicked(self, button, index, previous=False, next=False):
-        #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
-        newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
+        newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
         stack = hildon.WindowStack.get_default()
         if previous:
             tmp = stack.peek()
@@ -902,8 +1055,6 @@ class DisplayFeed(hildon.StackableWindow):
         if self.key == "ArchivedArticles":
             self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
-        self.ids.append(self.disp.connect("article-next", self.nextArticle))
-        self.ids.append(self.disp.connect("article-previous", self.previousArticle))
 
     def buttonPurgeArticles(self, *widget):
         self.clear()
@@ -925,22 +1076,6 @@ class DisplayFeed(hildon.StackableWindow):
                 break
             it = self.feedItems.iter_next(it)
 
-    def nextArticle(self, object, index):
-        self.mark_item_read(index)
-        id = self.feed.getNextId(index)
-        while id not in self.current and id != index:
-            id = self.feed.getNextId(id)
-        if id != index:
-            self.button_clicked(object, id, next=True)
-
-    def previousArticle(self, object, index):
-        self.mark_item_read(index)
-        id = self.feed.getPreviousId(index)
-        while id not in self.current and id != index:
-            id = self.feed.getPreviousId(id)
-        if id != index:
-            self.button_clicked(object, id, previous=True)
-
     def onArticleClosed(self, object, index):
         selection = self.feedList.get_selection()
         selection.set_mode(gtk.SELECTION_NONE)
@@ -952,8 +1087,12 @@ class DisplayFeed(hildon.StackableWindow):
         #self.feed.saveFeed(CONFIGDIR)
         self.displayFeed()
 
-    def button_update_clicked(self, button):
+
+    def do_update_feed(self):
         self.listing.updateFeed (self.key, priority=-1)
+
+    def button_update_clicked(self, button):
+        gobject.idle_add(self.do_update_feed)
             
     def show_download_bar(self):
         if not type(self.downloadDialog).__name__=="DownloadBar":
@@ -961,15 +1100,16 @@ class DisplayFeed(hildon.StackableWindow):
             self.downloadDialog.connect("download-done", self.onDownloadDone)
             self.main_vbox.pack_end(self.downloadDialog,
                                     expand=False, fill=False)
-            self.show_all()
-        
+            self.downloadDialog.show()
+
     def onDownloadDone(self, widget, feed):
-        if feed == self.feed or feed is None:
-            self.downloadDialog.destroy()
-            self.downloadDialog = False
+        if feed is not None and hasattr(self, 'feed') and feed == self.feed:
             self.feed = self.listing.getFeed(self.key)
             self.displayFeed()
-            self.updateDbusHandler.ArticleCountUpdated()
+
+        if feed is None:
+            self.downloadDialog.destroy()
+            self.downloadDialog = False
 
     def buttonReadAllClicked(self, button):
         #self.clear()
@@ -991,10 +1131,19 @@ class FeedingIt:
     def __init__(self):
         # Init the windows
         self.window = hildon.StackableWindow()
-        self.window.set_title(__appname__)
         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
-        self.mainVbox = gtk.VBox(False,10)
+
+        self.config = Config(self.window, CONFIGDIR+"config.ini")
+
+        try:
+            self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
+            self.orientation.set_mode(self.config.getOrientation())
+        except Exception, e:
+            logger.warn("Could not start rotation manager: %s" % str(e))
         
+        self.window.set_title(__appname__)
+        self.mainVbox = gtk.VBox(False,10)
+
         if isfile(CONFIGDIR+"/feeds.db"):           
             self.introLabel = gtk.Label("Loading...")
         else:
@@ -1004,38 +1153,66 @@ class FeedingIt:
 
         self.window.add(self.mainVbox)
         self.window.show_all()
-        self.config = Config(self.window, CONFIGDIR+"config.ini")
         gobject.idle_add(self.createWindow)
-        
+
     def createWindow(self):
         self.category = 0
-        
-        self.app_lock = get_lock("app_lock")
-        if self.app_lock == None:
-            try:
-                self.stopButton.set_sensitive(True)
-            except:
-                self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
-                self.stopButton.set_text("Stop update","")
-                self.stopButton.connect("clicked", self.stop_running_update)
-                self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
-                self.window.show_all()
-            self.introLabel.set_label("Update in progress, please wait.")
-            gobject.timeout_add_seconds(3, self.createWindow)
-            return False
-        try:
-            self.stopButton.destroy()
-        except:
-            pass
         self.listing = Listing(self.config, CONFIGDIR)
 
         self.downloadDialog = False
-        try:
-            self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
-            self.orientation.set_mode(self.config.getOrientation())
-        except:
-            print "Could not start rotation manager"
+
+        self.introLabel.destroy()
+        self.pannableListing = hildon.PannableArea()
+
+        # The main area is a view consisting of an icon and two
+        # strings.  The view is bound to the Listing's database via a
+        # TreeStore.
+
+        self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
+        self.feedList = gtk.TreeView(self.feedItems)
+        self.feedList.connect('row-activated', self.on_feedList_row_activated)
+
+        self.pannableListing.add(self.feedList)
+
+        icon_renderer = gtk.CellRendererPixbuf()
+        icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
+        icon_column = gtk.TreeViewColumn('', icon_renderer, \
+                pixbuf=COLUMN_ICON)
+        self.feedList.append_column(icon_column)
+
+        markup_renderer = gtk.CellRendererText()
+        markup_column = gtk.TreeViewColumn('', markup_renderer, \
+                markup=COLUMN_MARKUP)
+        self.feedList.append_column(markup_column)
+        self.mainVbox.pack_start(self.pannableListing)
+        self.mainVbox.show_all()
+
+        self.displayListing()
         
+        hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+        gobject.idle_add(self.late_init)
+
+    def update_progress(self, percent_complete,
+                        completed, in_progress, queued,
+                        bytes_downloaded, bytes_updated, bytes_per_second,
+                        updated_feed):
+        if (in_progress or queued) and not self.downloadDialog:
+            self.downloadDialog = DownloadBar(self.window)
+            self.downloadDialog.connect("download-done", self.onDownloadDone)
+            self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
+            self.downloadDialog.show()
+
+            if self.__dict__.get ('disp', None):
+                self.disp.show_download_bar ()
+
+    def onDownloadDone(self, widget, feed):
+        if feed is None:
+            self.downloadDialog.destroy()
+            self.downloadDialog = False
+            self.displayListing()
+
+    def late_init(self):
+        # Finish building the GUI.
         menu = hildon.AppMenu()
         # Create a button and add it to the menu
         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
@@ -1070,77 +1247,63 @@ class FeedingIt:
         
         self.window.set_app_menu(menu)
         menu.show_all()
-        
-        #self.feedWindow = hildon.StackableWindow()
-        #self.articleWindow = hildon.StackableWindow()
-        self.introLabel.destroy()
-        self.pannableListing = hildon.PannableArea()
-        self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
-        self.feedList = gtk.TreeView(self.feedItems)
-        self.feedList.connect('row-activated', self.on_feedList_row_activated)
-        #self.feedList.set_enable_tree_lines(True)                                                                                           
-        #self.feedList.set_show_expanders(True)
-        self.pannableListing.add(self.feedList)
 
-        icon_renderer = gtk.CellRendererPixbuf()
-        icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
-        icon_column = gtk.TreeViewColumn('', icon_renderer, \
-                pixbuf=COLUMN_ICON)
-        self.feedList.append_column(icon_column)
-
-        markup_renderer = gtk.CellRendererText()
-        markup_column = gtk.TreeViewColumn('', markup_renderer, \
-                markup=COLUMN_MARKUP)
-        self.feedList.append_column(markup_column)
-        self.mainVbox.pack_start(self.pannableListing)
-        self.mainVbox.show_all()
+        # Initialize the DBus interface.
+        self.dbusHandler = ServerObject(self)
+        bus = dbus.SessionBus()
+        bus.add_signal_receiver(handler_function=self.update_progress,
+                                bus_name=None,
+                                signal_name='UpdateProgress',
+                                dbus_interface='org.marcoz.feedingit',
+                                path='/org/marcoz/feedingit/update')
 
-        self.displayListing()
+        # Check whether auto-update is enabled.
         self.autoupdate = False
-        self.checkAutoUpdate()
-        
-        hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
-        gobject.idle_add(self.late_init)
-        
-    def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
-        if (not self.downloadDialog
-            and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
-            self.updateDbusHandler.UpdateStarted()
+        #self.checkAutoUpdate()
 
-            self.downloadDialog = DownloadBar(self.window)
-            self.downloadDialog.connect("download-done", self.onDownloadDone)
-            self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
-            self.mainVbox.show_all()
+        gobject.idle_add(self.build_feed_display)
+        gobject.idle_add(self.check_for_woodchuck)
 
-            if self.__dict__.get ('disp', None):
-                self.disp.show_download_bar ()
+    def build_feed_display(self):
+        if not hasattr(self, 'disp'):
+            self.disp = DisplayFeed(self.listing, self.config)
+            self.disp.connect("feed-closed", self.onFeedClosed)
 
-    def onDownloadDone(self, widget, feed):
-        if feed is None:
-            self.downloadDialog.destroy()
-            self.downloadDialog = False
-            self.displayListing()
-            self.updateDbusHandler.UpdateFinished()
-            self.updateDbusHandler.ArticleCountUpdated()
-
-    def stop_running_update(self, button):
-        self.stopButton.set_sensitive(False)
-        import dbus
-        bus=dbus.SessionBus()
-        remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
-                               "/org/marcoz/feedingit/update" # Object's path
-                              )
-        iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
-        iface.StopUpdate()
-    
-    def late_init(self):
-        self.dbusHandler = ServerObject(self)
-        self.updateDbusHandler = UpdateServerObject(self)
+    def check_for_woodchuck(self):
+        if self.config.getAskedAboutWoodchuck():
+            return
+
+        try:
+            import woodchuck
+            # It is already installed successfully.
+            self.config.setAskedAboutWoodchuck(True)
+            return
+        except ImportError:
+            pass
+
+        note = hildon.hildon_note_new_confirmation(
+            self.window,
+            "\nFeedingIt can use Woodchuck, a network transfer "
+            + "daemon, to schedule transfers more intelligently.\n"
+            + "You can also use the FeedingIt widget for time-based\n"
+            + "updates.\n\n"
+            + "Install Woodchuck?\n")
+        note.set_button_texts("Install", "Cancel")
+        note.add_button("Learn More", 42)
+
+        while True:
+            response = gtk.Dialog.run(note)
+            if response == 42:
+                open_in_browser("http://hssl.cs.jhu.edu/~neal/woodchuck")
+                continue
+
+            break
+
+        note.destroy()
 
-        jm = JobManager()
-        jm.stats_hook_register (self.job_manager_update,
-                                run_in_main_thread=True)
-        JobManager(True)
+        if response == gtk.RESPONSE_OK:
+            open_in_browser("http://maemo.org/downloads/product/raw/Maemo5/murmeltier?get_installfile")
+        self.config.setAskedAboutWoodchuck(True)
 
     def button_markAll(self, button):
         for key in self.listing.getListOfFeeds():
@@ -1177,7 +1340,7 @@ class FeedingIt:
         ret = wizard.run()
         if ret == 2:
             (title, url, category) = wizard.getData()
-            if (not title == '') and (not url == ''): 
+            if url:
                self.listing.addFeed(title, url, category=category)
         wizard.destroy()
         self.displayListing()
@@ -1187,17 +1350,17 @@ class FeedingIt:
             self.displayListing()
         SortList(self.window, self.listing, self, after_closing)
 
-    def button_update_clicked(self, button, key):
+    def do_update_feeds(self):
         for k in self.listing.getListOfFeeds():
             self.listing.updateFeed (k)
-        #self.displayListing()
+
+    def button_update_clicked(self, button, key):
+        gobject.idle_add(self.do_update_feeds)
 
     def onDownloadsDone(self, *widget):
         self.downloadDialog.destroy()
         self.downloadDialog = False
         self.displayListing()
-        self.updateDbusHandler.UpdateFinished()
-        self.updateDbusHandler.ArticleCountUpdated()
 
     def button_preferences_clicked(self, button):
         dialog = self.config.createDialog()
@@ -1264,8 +1427,6 @@ class FeedingIt:
                 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
                 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
                 updateTime = self.listing.getFeedUpdateTime(key)
-                if updateTime == 0:
-                    updateTime = "Never"
                 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
                 if unreadItems:
                     markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
@@ -1316,59 +1477,21 @@ class FeedingIt:
                 self.openFeed(key)
             
     def openFeed(self, key):
-        try:
-            self.feed_lock
-        except:
-            # If feed_lock doesn't exist, we can open the feed, else we do nothing
-            if key != None:
-                self.feed_lock = get_lock(key)
-                self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
-                        self.listing.getFeedTitle(key), key, \
-                        self.config, self.updateDbusHandler)
-                self.disp.connect("feed-closed", self.onFeedClosed)
+        if key != None:
+            self.build_feed_display()
+            self.disp.displayFeed(key)
                 
     def openArticle(self, key, id):
-        try:
-            self.feed_lock
-        except:
-            # If feed_lock doesn't exist, we can open the feed, else we do nothing
-            if key != None:
-                self.feed_lock = get_lock(key)
-                self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
-                        self.listing.getFeedTitle(key), key, \
-                        self.config, self.updateDbusHandler)
-                self.disp.button_clicked(None, id)
-                self.disp.connect("feed-closed", self.onFeedClosed)
-        
+        if key != None:
+            self.openFeed(key)
+            self.disp.button_clicked(None, id)
 
     def onFeedClosed(self, object, key):
-        #self.listing.saveConfig()
-        #del self.feed_lock
-        gobject.idle_add(self.onFeedClosedTimeout)
-        self.displayListing()
-        #self.updateDbusHandler.ArticleCountUpdated()
+        gobject.idle_add(self.displayListing)
         
-    def onFeedClosedTimeout(self):
-        del self.feed_lock
-        self.updateDbusHandler.ArticleCountUpdated()
-
     def quit(self, *args):
         self.window.hide()
-
-        if hasattr (self, 'app_lock'):
-            del self.app_lock
-
-        # Wait until all slave threads have properly exited before
-        # terminating the mainloop.
-        jm = JobManager()
-        jm.quit ()
-        stats = jm.stats()
-        if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
-            gtk.main_quit ()
-        else:
-            gobject.timeout_add(500, self.quit)
-
-        return False
+        gtk.main_quit ()
 
     def run(self):
         self.window.connect("destroy", self.quit)
@@ -1409,13 +1532,6 @@ class FeedingIt:
         self.button_update_clicked(None, None)
         return True
     
-    def stopUpdate(self):
-        # Not implemented in the app (see update_feeds.py)
-        try:
-            JobManager().cancel ()
-        except:
-            pass
-    
     def getStatus(self):
         status = ""
         for key in self.listing.getListOfFeeds():
@@ -1425,21 +1541,23 @@ class FeedingIt:
             status = "No unread items"
         return status
 
+    def grabFocus(self):
+        self.window.present()
+
 if __name__ == "__main__":
     mainthread.init ()
+    debugging.init(dot_directory=".feedingit", program_name="feedingit")
 
     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
-    gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
-    gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.threads_init()
     if not isdir(CONFIGDIR):
         try:
             mkdir(CONFIGDIR)
         except:
-            print "Error: Can't create configuration directory"
+            logger.error("Error: Can't create configuration directory")
             from sys import exit
             exit(1)
     app = FeedingIt()