psa: remove old event feed items
[feedingit] / src / FeedingIt.py
index 68fb7db..f9ab155 100644 (file)
@@ -25,7 +25,6 @@ __description__ = 'A simple RSS Reader for Maemo 5'
 # ============================================================================
 
 import gtk
-from pango import FontDescription
 import pango
 import hildon
 #import gtkhtml2
@@ -40,7 +39,6 @@ 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 config import Config
 from cgi import escape
@@ -151,6 +149,28 @@ def notify(message):
         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.
 #
@@ -331,7 +351,6 @@ 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):
@@ -807,35 +826,18 @@ class DisplayArticle(hildon.StackableWindow):
         if link == None:
             link = self.currentUrl
 
-        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())
+        open_in_browser(link)
 
 class DisplayFeed(hildon.StackableWindow):
-    def __init__(self, listing, feed, title, key, config):
+    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.config = config
+
         # Articles to show.
         #
         # If hide read articles is set, this is set to the set of
@@ -844,7 +846,6 @@ class DisplayFeed(hildon.StackableWindow):
         # but then decides to move back, previous should select the
         # just read article.
         self.articles = list()
-        self.config = config
         
         self.downloadDialog = False
         
@@ -862,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:
@@ -897,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()
-
-        self.pannableFeed = hildon.PannableArea()
+    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.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
+            if old_key:
+                self.listing.updateUnread(self.key)
+                self.emit("feed-closed", self.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.feed = self.listing.getFeed(key)
+            self.feedTitle = self.listing.getFeedTitle(key)
+            self.key = key
 
-        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.set_title(self.feedTitle)
 
-        
-        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:
@@ -959,14 +989,14 @@ class DisplayFeed(hildon.StackableWindow):
         else:
             articles = self.feed.getIds()
         
-        hasArticle = False
         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.articles.append(id)
@@ -976,21 +1006,22 @@ class DisplayFeed(hildon.StackableWindow):
                     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)
@@ -1069,10 +1100,10 @@ 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:
+        if feed is not None and hasattr(self, 'feed') and feed == self.feed:
             self.feed = self.listing.getFeed(self.key)
             self.displayFeed()
 
@@ -1100,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:
@@ -1113,7 +1153,6 @@ 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):
@@ -1121,56 +1160,18 @@ class FeedingIt:
         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 Exception, e:
-            logger.warn("Could not start rotation manager: %s" % str(e))
-        
-        menu = hildon.AppMenu()
-        # Create a button and add it to the menu
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Update feeds")
-        button.connect("clicked", self.button_update_clicked, "All")
-        menu.append(button)
-        
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Mark all as read")
-        button.connect("clicked", self.button_markAll)
-        menu.append(button)
-
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Add new feed")
-        button.connect("clicked", lambda b: self.addFeed())
-        menu.append(button)
-
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Manage subscriptions")
-        button.connect("clicked", self.button_organize_clicked)
-        menu.append(button)
 
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Settings")
-        button.connect("clicked", self.button_preferences_clicked)
-        menu.append(button)
-       
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("About")
-        button.connect("clicked", self.button_about_clicked)
-        menu.append(button)
-        
-        self.window.set_app_menu(menu)
-        menu.show_all()
-        
-        #self.feedWindow = hildon.StackableWindow()
-        #self.articleWindow = hildon.StackableWindow()
         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.feedList.set_enable_tree_lines(True)                                                                                           
-        #self.feedList.set_show_expanders(True)
+
         self.pannableListing.add(self.feedList)
 
         icon_renderer = gtk.CellRendererPixbuf()
@@ -1187,12 +1188,10 @@ class FeedingIt:
         self.mainVbox.show_all()
 
         self.displayListing()
-        self.autoupdate = False
-        self.checkAutoUpdate()
         
         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,
@@ -1201,7 +1200,7 @@ class FeedingIt:
             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()
+            self.downloadDialog.show()
 
             if self.__dict__.get ('disp', None):
                 self.disp.show_download_bar ()
@@ -1213,6 +1212,43 @@ class FeedingIt:
             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)
+        button.set_label("Update feeds")
+        button.connect("clicked", self.button_update_clicked, "All")
+        menu.append(button)
+        
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Mark all as read")
+        button.connect("clicked", self.button_markAll)
+        menu.append(button)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Add new feed")
+        button.connect("clicked", lambda b: self.addFeed())
+        menu.append(button)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Manage subscriptions")
+        button.connect("clicked", self.button_organize_clicked)
+        menu.append(button)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Settings")
+        button.connect("clicked", self.button_preferences_clicked)
+        menu.append(button)
+       
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("About")
+        button.connect("clicked", self.button_about_clicked)
+        menu.append(button)
+        
+        self.window.set_app_menu(menu)
+        menu.show_all()
+
+        # Initialize the DBus interface.
         self.dbusHandler = ServerObject(self)
         bus = dbus.SessionBus()
         bus.add_signal_receiver(handler_function=self.update_progress,
@@ -1221,6 +1257,54 @@ class FeedingIt:
                                 dbus_interface='org.marcoz.feedingit',
                                 path='/org/marcoz/feedingit/update')
 
+        # Check whether auto-update is enabled.
+        self.autoupdate = False
+        #self.checkAutoUpdate()
+
+        gobject.idle_add(self.build_feed_display)
+        gobject.idle_add(self.check_for_woodchuck)
+
+    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 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()
+
+        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():
             feed = self.listing.getFeed(key)
@@ -1343,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)
@@ -1396,11 +1478,8 @@ class FeedingIt:
             
     def openFeed(self, key):
         if key != None:
-            self.disp = DisplayFeed(
-                self.listing, self.listing.getFeed(key),
-                self.listing.getFeedTitle(key), key,
-                self.config)
-            self.disp.connect("feed-closed", self.onFeedClosed)
+            self.build_feed_display()
+            self.disp.displayFeed(key)
                 
     def openArticle(self, key, id):
         if key != None:
@@ -1408,7 +1487,7 @@ class FeedingIt:
             self.disp.button_clicked(None, id)
 
     def onFeedClosed(self, object, key):
-        self.displayListing()
+        gobject.idle_add(self.displayListing)
         
     def quit(self, *args):
         self.window.hide()