Ask the user whether they'd like to install Woodchuck.
[feedingit] / src / FeedingIt.py
index f6706a7..29dc7fa 100644 (file)
@@ -2,6 +2,7 @@
 
 # 
 # Copyright (c) 2007-2008 INdT.
+# Copyright (c) 2011 Neal H. Walfield
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
 #
 
 # ============================================================================
-# Name        : FeedingIt.py
-# Author      : Yves Marcoz
-# Version     : 0.1
-# Description : PyGtk Example 
+__appname__ = 'FeedingIt'
+__author__  = 'Yves Marcoz'
+__version__ = '0.9.1~woodchuck'
+__description__ = 'A simple RSS Reader for Maemo 5'
 # ============================================================================
 
 import gtk
-import feedparser
 import pango
 import hildon
-import gtkhtml2
-import time
-import webbrowser
-import pickle
-from os.path import isfile, isdir
-from os import mkdir
-import md5
-import sys   
-import urllib2
-
-from rss import *
-   
-class AddWidgetWizard(hildon.WizardDialog):
+#import gtkhtml2
+#try:
+from webkit import WebView
+#    has_webkit=True
+#except:
+#    import gtkhtml2
+#    has_webkit=False
+from os.path import isfile, isdir, exists
+from os import mkdir, remove, stat, environ
+import gobject
+from aboutdialog import HeAboutDialog
+from portrait import FremantleRotation
+from feedingitdbus import ServerObject
+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 socket import setdefaulttimeout
+timeout = 5
+setdefaulttimeout(timeout)
+del timeout
+
+import xml.sax
+
+LIST_ICON_SIZE = 32
+LIST_ICON_BORDER = 10
+
+USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
+ABOUT_ICON = 'feedingit'
+ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
+ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
+ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
+ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
+
+color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
+unread_color = color_style.lookup_color('ActiveTextColor')
+read_color = color_style.lookup_color('DefaultTextColor')
+del color_style
+
+CONFIGDIR="/home/user/.feedingit/"
+LOCK = CONFIGDIR + "update.lock"
+
+from re import sub
+from htmlentitydefs import name2codepoint
+
+COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
+
+FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
+
+import style
+
+MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
+MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
+MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
+
+# Build the markup template for the Maemo 5 text style
+head_font = style.get_font_desc('SystemFont')
+sub_font = style.get_font_desc('SmallSystemFont')
+
+#head_color = style.get_color('ButtonTextColor')
+head_color = style.get_color('DefaultTextColor')
+sub_color = style.get_color('DefaultTextColor')
+active_color = style.get_color('ActiveTextColor')
+
+bg_color = style.get_color('DefaultBackgroundColor').to_string()
+c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
+c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
+c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
+bg_color = "#" + c1 + c2 + c3
+
+
+head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
+normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
+
+entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
+entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
+
+active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
+active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
+
+entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
+entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
+
+FEED_TEMPLATE = '\n'.join((head, normal_sub))
+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.
+#
+# @param text The HTML (or XML) source text.
+# @return The plain text, as a Unicode string, if necessary.
+# http://effbot.org/zone/re-sub.htm#unescape-html
+def unescape(text):
+    def fixup(m):
+        text = m.group(0)
+        if text[:2] == "&#":
+            # character reference
+            try:
+                if text[:3] == "&#x":
+                    return unichr(int(text[3:-1], 16))
+                else:
+                    return unichr(int(text[2:-1]))
+            except ValueError:
+                pass
+        else:
+            # named entity
+            try:
+                text = unichr(name2codepoint[text[1:-1]])
+            except KeyError:
+                pass
+        return text # leave as is
+    return sub("&#?\w+;", fixup, text)
+
+
+class AddWidgetWizard(gtk.Dialog):
+    def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
+        gtk.Dialog.__init__(self)
+        self.set_transient_for(parent)
+        
+        #self.category = categories[0]
+        self.category = currentCat
+
+        if isEdit:
+            self.set_title('Edit RSS feed')
+        else:
+            self.set_title('Add new RSS feed')
+
+        if isEdit:
+            self.btn_add = self.add_button('Save', 2)
+        else:
+            self.btn_add = self.add_button('Add', 2)
+
+        self.set_default_response(2)
+
+        self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
+        self.nameEntry.set_placeholder('Feed name')
+        # 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)
+
+        self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
+        self.urlEntry.set_placeholder('Feed URL')
+        self.urlEntry.set_text(urlIn)
+        self.urlEntry.select_region(-1, -1)
+        self.urlEntry.set_activates_default(True)
+
+        self.table = gtk.Table(3, 2, False)
+        self.table.set_col_spacings(5)
+        label = gtk.Label('Name:')
+        label.set_alignment(1., .5)
+        self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
+        self.table.attach(self.nameEntry, 1, 2, 0, 1)
+        label = gtk.Label('URL:')
+        label.set_alignment(1., .5)
+        self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
+        self.table.attach(self.urlEntry, 1, 2, 1, 2)
+        selector = self.create_selector(categories, listing)
+        picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
+        picker.set_selector(selector)
+        picker.set_title("Select category")
+        #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
+        picker.set_name('HildonButton-finger')
+        picker.set_alignment(0,0,1,1)
+        
+        self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
+        
+        self.vbox.pack_start(self.table)
+
+        self.show_all()
+
+    def getData(self):
+        return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
     
-    def __init__(self, parent):
-        # Create a Notebook
-        self.notebook = gtk.Notebook()
+    def create_selector(self, choices, listing):
+        #self.pickerDialog = hildon.PickerDialog(self.parent)
+        selector = hildon.TouchSelector(text=True)
+        index = 0
+        self.map = {}
+        for item in choices:
+            title = listing.getCategoryTitle(item)
+            iter = selector.append_text(str(title))
+            if self.category == item: 
+                selector.set_active(0, index)
+            self.map[title] = item
+            index += 1
+        selector.connect("changed", self.selection_changed)
+        #self.pickerDialog.set_selector(selector)
+        return selector
+
+    def selection_changed(self, selector, button):
+        current_selection = selector.get_current_text()
+        if current_selection:
+            self.category = self.map[current_selection]
+
+class AddCategoryWizard(gtk.Dialog):
+    def __init__(self, parent, titleIn=None, isEdit=False):
+        gtk.Dialog.__init__(self)
+        self.set_transient_for(parent)
+
+        if isEdit:
+            self.set_title('Edit Category')
+        else:
+            self.set_title('Add Category')
+
+        if isEdit:
+            self.btn_add = self.add_button('Save', 2)
+        else:
+            self.btn_add = self.add_button('Add', 2)
+
+        self.set_default_response(2)
 
         self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
-        self.nameEntry.set_placeholder("Enter Feed Name")
+        self.nameEntry.set_placeholder('Category name')
+        if not titleIn == None:
+            self.nameEntry.set_text(titleIn)
+            self.nameEntry.select_region(-1, -1)
+
+        self.table = gtk.Table(1, 2, False)
+        self.table.set_col_spacings(5)
+        label = gtk.Label('Name:')
+        label.set_alignment(1., .5)
+        self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
+        self.table.attach(self.nameEntry, 1, 2, 0, 1)
+        #label = gtk.Label('URL:')
+        #label.set_alignment(1., .5)
+        #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
+        #self.table.attach(self.urlEntry, 1, 2, 1, 2)
+        self.vbox.pack_start(self.table)
+
+        self.show_all()
+
+    def getData(self):
+        return self.nameEntry.get_text()
         
-        self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
+class DownloadBar(gtk.ProgressBar):
+    @classmethod
+    def class_init(cls):
+        if hasattr (cls, 'class_init_done'):
+            return
+
+        cls.downloadbars = []
+        # Total number of jobs we are monitoring.
+        cls.total = 0
+        # Number of jobs complete (of those that we are monitoring).
+        cls.done = 0
+        # Percent complete.
+        cls.progress = 0
+
+        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 ()
+
+        gtk.ProgressBar.__init__(self)
+
+        self.downloadbars.append(weakref.ref (self))
+        self.set_fraction(0)
+        self.__class__.update_bars()
+
+    @classmethod
+    def downloading(cls):
+        cls.class_init ()
+        return cls.done != cls.total
+
+    @classmethod
+    def update_progress(cls, percent_complete,
+                        completed, in_progress, queued,
+                        bytes_downloaded, bytes_updated, bytes_per_second,
+                        feed_updated):
+        if not cls.downloadbars:
+            return
+
+        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", feed_updated)
+
+        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", None)
+            return
+
+        cls.update_bars()
+
+    @classmethod
+    def update_bars(cls):
+        # In preparation for i18n/l10n
+        def N_(a, b, n):
+            return (a if n == 1 else b)
+
+        text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
+                   cls.total)
+                % (cls.done, cls.total))
+
+        for ref in cls.downloadbars:
+            bar = ref ()
+            if bar is None:
+                # The download bar disappeared.
+                cls.downloadbars.remove (ref)
+            else:
+                bar.set_text(text)
+                bar.set_fraction(cls.progress)
+
+class SortList(hildon.StackableWindow):
+    def __init__(self, parent, listing, feedingit, after_closing, category=None):
+        hildon.StackableWindow.__init__(self)
+        self.set_transient_for(parent)
+        if category:
+            self.isEditingCategories = False
+            self.category = category
+            self.set_title(listing.getCategoryTitle(category))
+        else:
+            self.isEditingCategories = True
+            self.set_title('Categories')
+        self.listing = listing
+        self.feedingit = feedingit
+        self.after_closing = after_closing
+        if after_closing:
+            self.connect('destroy', lambda w: self.after_closing())
+        self.vbox2 = gtk.VBox(False, 2)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
+        button.connect("clicked", self.buttonUp)
+        self.vbox2.pack_start(button, expand=False, fill=False)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
+        button.connect("clicked", self.buttonDown)
+        self.vbox2.pack_start(button, expand=False, fill=False)
+
+        self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
+        button.connect("clicked", self.buttonAdd)
+        self.vbox2.pack_start(button, expand=False, fill=False)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
+        button.connect("clicked", self.buttonEdit)
+        self.vbox2.pack_start(button, expand=False, fill=False)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
+        button.connect("clicked", self.buttonDelete)
+        self.vbox2.pack_start(button, expand=False, fill=False)
+
+        #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        #button.set_label("Done")
+        #button.connect("clicked", self.buttonDone)
+        #self.vbox.pack_start(button)
+        self.hbox2= gtk.HBox(False, 10)
+        self.pannableArea = hildon.PannableArea()
+        self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+        self.treeview = gtk.TreeView(self.treestore)
+        self.hbox2.pack_start(self.pannableArea, expand=True)
+        self.displayFeeds()
+        self.hbox2.pack_end(self.vbox2, expand=False)
+        self.set_default_size(-1, 600)
+        self.add(self.hbox2)
+
+        menu = hildon.AppMenu()
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Import from OPML")
+        button.connect("clicked", self.feedingit.button_import_clicked)
+        menu.append(button)
+
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Export to OPML")
+        button.connect("clicked", self.feedingit.button_export_clicked)
+        menu.append(button)
+        self.set_app_menu(menu)
+        menu.show_all()
         
-        self.urlEntry.set_placeholder("Enter a URL")
-            
-        labelEnd = gtk.Label("Success")
-        
-        self.notebook.append_page(self.nameEntry, None)
-        self.notebook.append_page(self.urlEntry, None) 
-        self.notebook.append_page(labelEnd, None)      
-
-        hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
-   
-        # Set a handler for "switch-page" signal
-        #self.notebook.connect("switch_page", self.on_page_switch, self)
-   
-        # Set a function to decide if user can go to next page
-        self.set_forward_page_func(self.some_page_func)
-   
         self.show_all()
-        print dir(self)
+        #self.connect("destroy", self.buttonDone)
         
-    def getData(self):
-        return (self.nameEntry.get_text(), self.urlEntry.get_text())
+    def displayFeeds(self):
+        self.treeview.destroy()
+        self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+        self.treeview = gtk.TreeView()
         
-    def on_page_switch(self, notebook, page, num, dialog):
-        print >>sys.stderr, "Page %d" % num
-        return True
-   
-    def some_page_func(self, nb, current, userdata):
-        # Validate data for 1st page
-        print current
-        if current == 0:
-            entry = nb.get_nth_page(current)
-            # Check the name is not null
-            return len(entry.get_text()) != 0
-        elif current == 1:
-            entry = nb.get_nth_page(current)
-            # Check the url is not null, and starts with http
-            print ( (len(entry.get_text()) != 0) and (entry.get_text().startswith("http")) )
-            return ( (len(entry.get_text()) != 0) and (entry.get_text().startswith("http")) )
-        elif current != 2:
+        self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
+        hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
+        self.refreshList()
+        self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
+
+        self.pannableArea.add(self.treeview)
+
+        #self.show_all()
+
+    def refreshList(self, selected=None, offset=0):
+        #rect = self.treeview.get_visible_rect()
+        #y = rect.y+rect.height
+        self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+        if self.isEditingCategories:
+            for key in self.listing.getListOfCategories():
+                item = self.treestore.append([self.listing.getCategoryTitle(key), key])
+                if key == selected:
+                    selectedItem = item
+        else:
+            for key in self.listing.getListOfFeeds(category=self.category):
+                item = self.treestore.append([self.listing.getFeedTitle(key), key])
+                if key == selected:
+                    selectedItem = item
+        self.treeview.set_model(self.treestore)
+        if not selected == None:
+            self.treeview.get_selection().select_iter(selectedItem)
+            self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
+        self.pannableArea.show_all()
+
+    def getSelectedItem(self):
+        (model, iter) = self.treeview.get_selection().get_selected()
+        if not iter:
+            return None
+        return model.get_value(iter, 1)
+
+    def findIndex(self, key):
+        after = None
+        before = None
+        found = False
+        for row in self.treestore:
+            if found:
+                return (before, row.iter)
+            if key == list(row)[0]:
+                found = True
+            else:
+                before = row.iter
+        return (before, None)
+
+    def buttonUp(self, button):
+        key  = self.getSelectedItem()
+        if not key == None:
+            if self.isEditingCategories:
+                self.listing.moveCategoryUp(key)
+            else:
+                self.listing.moveUp(key)
+            self.refreshList(key, -10)
+
+    def buttonDown(self, button):
+        key = self.getSelectedItem()
+        if not key == None:
+            if self.isEditingCategories:
+                self.listing.moveCategoryDown(key)
+            else:
+                self.listing.moveDown(key)
+            self.refreshList(key, 10)
+
+    def buttonDelete(self, button):
+        key = self.getSelectedItem()
+
+        message = 'Really remove this feed and its entries?'
+        dlg = hildon.hildon_note_new_confirmation(self, message)
+        response = dlg.run()
+        dlg.destroy()
+        if response == gtk.RESPONSE_OK:
+            if self.isEditingCategories:
+                self.listing.removeCategory(key)
+            else:
+                self.listing.removeFeed(key)
+            self.refreshList()
+
+    def buttonEdit(self, button):
+        key = self.getSelectedItem()
+
+        if key == 'ArchivedArticles':
+            message = 'Cannot edit the archived articles feed.'
+            hildon.hildon_banner_show_information(self, '', message)
+            return
+        if self.isEditingCategories:
+            if key is not None:
+                SortList(self.parent, self.listing, self.feedingit, None, category=key)
+        else:
+            if key is not None:
+                wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
+                ret = wizard.run()
+                if ret == 2:
+                    (title, url, category) = wizard.getData()
+                    if url != '':
+                        self.listing.editFeed(key, title, url, category=category)
+                        self.refreshList()
+                wizard.destroy()
+
+    def buttonDone(self, *args):
+        self.destroy()
+        
+    def buttonAdd(self, button, urlIn="http://"):
+        if self.isEditingCategories:
+            wizard = AddCategoryWizard(self)
+            ret = wizard.run()
+            if ret == 2:
+                title = wizard.getData()
+                if (not title == ''): 
+                   self.listing.addCategory(title)
+        else:
+            wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
+            ret = wizard.run()
+            if ret == 2:
+                (title, url, category) = wizard.getData()
+                if url:
+                   self.listing.addFeed(title, url, category=category)
+        wizard.destroy()
+        self.refreshList()
+               
+
+class DisplayArticle(hildon.StackableWindow):
+    """
+    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.article_id = None
+        self.feed = feed
+        self.feed_key = feed_key
+        self.articles = articles
+        self.config = config
+        self.listing = listing
+
+        self.set_title(self.listing.getFeedTitle(feed_key))
+
+        # Init the article display
+        self.view = WebView()
+        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.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()
+
+        def menu_button(label, callback):
+            button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+            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:
+            menu_button("Add to archived articles", self.archive_button)
+        
+        self.set_app_menu(menu)
+        menu.show_all()
+        
+        self.destroyId = self.connect("destroy", self.destroyWindow)
+
+        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
         else:
+            return False
+
+    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()
+        if self.loadedArticle:
+            self.currentUrl = frame.get_uri()
+        else:
+            self.loadedArticle = True
+
+    def button_pressed(self, window, event):
+        """
+        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):
+        x = self.coords[0] - event.x
+        y = self.coords[1] - event.y
+        
+        if (2*abs(y) < abs(x)):
+            if (x > 15):
+                self.article_next(forward=False)
+            elif (x<-15):
+                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)
+        self.destroy()
+        
+    def horiz_scrolling_button(self, *widget):
+        self.pannable_article.disconnect(self.gestureId)
+        self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
+        
+    def archive_button(self, *widget):
+        # Call the listing.addArchivedArticle
+        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):
+        """
+        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:
+            link = self.currentUrl
+
+        open_in_browser(link)
+
+class DisplayFeed(hildon.StackableWindow):
+    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.config = config
+
+        # 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
+        
+        #self.listing.setCurrentlyDisplayedFeed(self.key)
+        
+        self.disp = False
+        
+        menu = hildon.AppMenu()
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Update feed")
+        button.connect("clicked", self.button_update_clicked)
+        menu.append(button)
+        
+        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        button.set_label("Mark all as read")
+        button.connect("clicked", self.buttonReadAllClicked)
+        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.feedItems = gtk.ListStore(str, str)
+
+        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)
+
+        vbox = gtk.VBox(False, 10)
+        vbox.pack_start(self.feedList)
+        
+        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:
+            return
+
+        # Fix up the column width for wrapping the text when the window is
+        # resized (i.e. orientation changed)
+        self.markup_renderer.set_property('wrap-width', event.width-20)  
+        it = self.feedItems.get_iter_first()
+        while it is not None:
+            markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
+            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):
+        try:
+            key = self.key
+        except AttributeError:
+            key = None
+
+        self.destroy()
+
+    def fix_title(self, title):
+        return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
+
+    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
+
+            if old_key:
+                self.listing.updateUnread(self.key)
+                self.emit("feed-closed", self.key)
+
+            self.feed = self.listing.getFeed(key)
+            self.feedTitle = self.listing.getFeedTitle(key)
+            self.key = key
+
+            self.set_title(self.feedTitle)
+
+            selection = self.feedList.get_selection()
+            if selection is not None:
+                selection.set_mode(gtk.SELECTION_NONE)
+
+            for b in self.archived_article_buttons:
+                if key == "ArchivedArticles":
+                    b.show()
+                else:
+                    b.hide()
+        
+        #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
+        hideReadArticles = self.config.getHideReadArticles()
+        if hideReadArticles:
+            articles = self.feed.getIds(onlyUnread=True)
+        else:
+            articles = self.feed.getIds()
+        
+        self.articles[:] = []
+
+        self.feedItems.clear()
+        for id in articles:
+            try:
+                isRead = self.feed.isEntryRead(id)
+            except Exception:
+                isRead = False
+            if not ( isRead and hideReadArticles ):
+                title = self.fix_title(self.feed.getTitle(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))
+        if not articles:
+            # No articles.
+            markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
+            self.feedItems.append((markup, ""))
+
+        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)
+        model = treeview.get_model()
+        iter = model.get_iter(path)
+        key = model.get_value(iter, FEED_COLUMN_KEY)
+        # Emulate legacy "button_clicked" call via treeview
+        gobject.idle_add(self.button_clicked, treeview, key)
+        #return True
+
+    def button_clicked(self, button, index, previous=False, next=False):
+        newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
+        stack = hildon.WindowStack.get_default()
+        if previous:
+            tmp = stack.peek()
+            stack.pop_and_push(1, newDisp, tmp)
+            newDisp.show()
+            gobject.timeout_add(200, self.destroyArticle, tmp)
+            #print "previous"
+            self.disp = newDisp
+        elif next:
+            newDisp.show_all()
+            if type(self.disp).__name__ == "DisplayArticle":
+                gobject.timeout_add(200, self.destroyArticle, self.disp)
+            self.disp = newDisp
+        else:
+            self.disp = newDisp
+            self.disp.show_all()
+        
+        self.ids = []
+        if self.key == "ArchivedArticles":
+            self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
+        self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
+
+    def buttonPurgeArticles(self, *widget):
+        self.clear()
+        self.feed.purgeReadArticles()
+        #self.feed.saveFeed(CONFIGDIR)
+        self.displayFeed()
+
+    def destroyArticle(self, handle):
+        handle.destroyWindow()
+
+    def mark_item_read(self, key):
+        it = self.feedItems.get_iter_first()
+        while it is not None:
+            k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
+            if k == key:
+                title = self.fix_title(self.feed.getTitle(key))
+                markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
+                self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
+                break
+            it = self.feedItems.iter_next(it)
+
+    def onArticleClosed(self, object, index):
+        selection = self.feedList.get_selection()
+        selection.set_mode(gtk.SELECTION_NONE)
+        self.mark_item_read(index)
+
+    def onArticleDeleted(self, object, index):
+        self.clear()
+        self.feed.removeArticle(index)
+        #self.feed.saveFeed(CONFIGDIR)
+        self.displayFeed()
+
+
+    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":
+            self.downloadDialog = DownloadBar(self.window)
+            self.downloadDialog.connect("download-done", self.onDownloadDone)
+            self.main_vbox.pack_end(self.downloadDialog,
+                                    expand=False, fill=False)
+            self.downloadDialog.show()
+
+    def onDownloadDone(self, widget, feed):
+        if feed is not None and hasattr(self, 'feed') and feed == self.feed:
+            self.feed = self.listing.getFeed(self.key)
+            self.displayFeed()
+
+        if feed is None:
+            self.downloadDialog.destroy()
+            self.downloadDialog = False
+
+    def buttonReadAllClicked(self, button):
+        #self.clear()
+        self.feed.markAllAsRead()
+        it = self.feedItems.get_iter_first()
+        while it is not None:
+            k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
+            title = self.fix_title(self.feed.getTitle(k))
+            markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
+            self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
+            it = self.feedItems.iter_next(it)
+        #self.displayFeed()
+        #for index in self.feed.getIds():
+        #    self.feed.setEntryRead(index)
+        #    self.mark_item_read(index)
+
+
 class FeedingIt:
     def __init__(self):
         # Init the windows
         self.window = hildon.StackableWindow()
+        hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
+
+        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:
+            self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
+        
+        self.mainVbox.pack_start(self.introLabel)
+
+        self.window.add(self.mainVbox)
+        self.window.show_all()
+        gobject.idle_add(self.createWindow)
+
+    def createWindow(self):
+        self.category = 0
+        self.listing = Listing(self.config, CONFIGDIR)
+
+        self.downloadDialog = False
+
+        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)
-        button.set_label("Update All Feeds")
+        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("Add Feed")
-        button.connect("clicked", self.button_add_clicked)
+        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.listing = Listing()
-        #self.listing.downloadFeeds()
-        self.displayListing() 
-        
-        #self.window.show_all()
-        #self.displayFeed(self.listing.getFeed(0))
+        # 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')
+
+        # 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\n"
+            + "Install Woodchuck? (This is recommended.)\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)
+            feed.markAllAsRead()
+            #for id in feed.getIds():
+            #    feed.setEntryRead(id)
+            self.listing.updateUnread(key)
+        self.displayListing()
+
+    def button_about_clicked(self, button):
+        HeAboutDialog.present(self.window, \
+                __appname__, \
+                ABOUT_ICON, \
+                __version__, \
+                __description__, \
+                ABOUT_COPYRIGHT, \
+                ABOUT_WEBSITE, \
+                ABOUT_BUGTRACKER, \
+                ABOUT_DONATE)
+
+    def button_export_clicked(self, button):
+        opml = ExportOpmlData(self.window, self.listing)
         
-    def button_add_clicked(self, button):
-        wizard = AddWidgetWizard(self.window)
+    def button_import_clicked(self, button):
+        opml = GetOpmlData(self.window)
+        feeds = opml.getData()
+        for (title, url) in feeds:
+            self.listing.addFeed(title, url)
+        self.displayListing()
+
+    def addFeed(self, urlIn="http://"):
+        wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
         ret = wizard.run()
         if ret == 2:
-            (title, url) = wizard.getData()
-            if (not title == '') and (not url == ''): 
-               self.listing.addFeed(title, url)
+            (title, url, category) = wizard.getData()
+            if url:
+               self.listing.addFeed(title, url, category=category)
         wizard.destroy()
         self.displayListing()
-        
+
+    def button_organize_clicked(self, button):
+        def after_closing():
+            self.displayListing()
+        SortList(self.window, self.listing, self, after_closing)
+
+    def do_update_feeds(self):
+        for k in self.listing.getListOfFeeds():
+            self.listing.updateFeed (k)
+
     def button_update_clicked(self, button, key):
-        hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
-        if key == "All":
-            self.listing.updateFeeds()
-        else:
-            self.listing.getFeed(key).updateFeed()
+        gobject.idle_add(self.do_update_feeds)
+
+    def onDownloadsDone(self, *widget):
+        self.downloadDialog.destroy()
+        self.downloadDialog = False
         self.displayListing()
-        hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+
+    def button_preferences_clicked(self, button):
+        dialog = self.config.createDialog()
+        dialog.connect("destroy", self.prefsClosed)
+
+    def show_confirmation_note(self, parent, title):
+        note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
+
+        retcode = gtk.Dialog.run(note)
+        note.destroy()
+        
+        if retcode == gtk.RESPONSE_OK:
+            return True
+        else:
+            return False
+        
+    def saveExpandedLines(self):
+       self.expandedLines = []
+       model = self.feedList.get_model()
+       model.foreach(self.checkLine)
+
+    def checkLine(self, model, path, iter, data = None):
+       if self.feedList.row_expanded(path):
+           self.expandedLines.append(path)
+
+    def restoreExpandedLines(self):
+       model = self.feedList.get_model()
+       model.foreach(self.restoreLine)
+
+    def restoreLine(self, model, path, iter, data = None):
+       if path in self.expandedLines:
+           self.feedList.expand_row(path, False)
         
     def displayListing(self):
-        try:
-            self.window.remove(self.pannableListing)
-        except:
-            pass
-        self.vboxListing = gtk.VBox(False,10)
-        self.pannableListing = hildon.PannableArea()
-        self.pannableListing.add_with_viewport(self.vboxListing)
+        icon_theme = gtk.icon_theme_get_default()
+        default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
+                gtk.ICON_LOOKUP_USE_BUILTIN)
 
-        for key in self.listing.getListOfFeeds():
-            #button = gtk.Button(item)
-            button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
-                              hildon.BUTTON_ARRANGEMENT_VERTICAL)
-            button.set_text(self.listing.getFeedTitle(key), self.listing.getFeedUpdateTime(key))
-            button.set_alignment(0,0,1,1)
-            #label = button.child
-            #label.modify_font(pango.FontDescription("sans 10"))
-            button.connect("clicked", self.buttonFeedClicked, self, self.window, key)
-            self.vboxListing.pack_start(button, expand=False)
-        self.window.add(self.pannableListing)
-        self.window.show_all()
+        self.saveExpandedLines()
+
+        self.feedItems.clear()
+        hideReadFeed = self.config.getHideReadFeeds()
+        order = self.config.getFeedSortOrder()
         
-    def displayFeed(self, key):
-        # Initialize the feed panel
-        self.vboxFeed = gtk.VBox(False, 10)
-        self.pannableFeed = hildon.PannableArea()
-        self.pannableFeed.add_with_viewport(self.vboxFeed)
+        categories = self.listing.getListOfCategories()
+        if len(categories) > 1:
+            showCategories = True
+        else:
+            showCategories = False
         
-        index = 0
-        for item in self.listing.getFeed(key).getEntries():
-            #button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
-            #                  hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
-            #button.set_text(item["title"], time.strftime("%a, %d %b %Y %H:%M:%S",item["updated_parsed"]))
-            #button.set_text(item["title"], time.asctime(item["updated_parsed"]))
-            #button.set_text(item["title"],"")
-            #button.set_alignment(0,0,1,1)
-            #button.set_markup(True)
-            button = gtk.Button(item["title"])
-            button.set_alignment(0,0)
-            label = button.child
-            #label.set_markup(item["title"])
-            label.modify_font(pango.FontDescription("sans 16"))
-            button.connect("clicked", self.button_clicked, self, self.window, key, index)
+        for categoryId in categories:
+        
+            title = self.listing.getCategoryTitle(categoryId)
+            keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
             
-            self.vboxFeed.pack_start(button, expand=False)
-            index=index+1
-
-        self.feedWindow.add(self.pannableFeed)
-        self.feedWindow.show_all()
-     
-    def displayArticle(self, key, index):
-        text = self.listing.getFeed(key).getArticle(index)
-        self.articleWindow = hildon.StackableWindow()
-
-        # Init the article display    
-        self.view = gtkhtml2.View()
-        self.pannable_article = hildon.PannableArea()
-        self.pannable_article.add(self.view)
-        self.document = gtkhtml2.Document()
-        self.view.set_document(self.document)
-        
-        self.document.clear()
-        self.document.open_stream("text/html")
-        self.document.write_stream(text)
-        self.document.close_stream()
-        
-        self.articleWindow.add(self.pannable_article)
-        
-        self.articleWindow.show_all()
-        
-        self.document.connect("link_clicked", self._signal_link_clicked)
-        self.document.connect("request-url", self._signal_request_url)
-        
-        self.document.open_stream("text/html")
-        self.document.write_stream(text)
-        self.document.close_stream()
-        self.document.show()
-     
-    def tap(self):
-        pass
-#    def _signal_on_url(self, object, url):
-#        if url == None: url = ""
-#        else: url = self._complete_url(url)
-        #self.emit("status_changed", url)
-
-    def _signal_link_clicked(self, object, link):
-        webbrowser.open(link)
-
-    def _signal_request_url(self, object, url, stream):
-        f = urllib2.urlopen(url)
-        stream.write(f.read())
-        stream.close()
-#        
-#    def _complete_url(self, url):
-#        import string, urlparse, urllib
-#        url = urllib.quote(url, safe=string.punctuation)
-#        if urlparse.urlparse(url)[0] == '':
-#            return urlparse.urljoin(self.location, url)
-#        else:
-#            return url
-#        
-#    def _open_url(self, url, headers=[]):
-#        import urllib2
-#        opener = urllib2.build_opener()
-#        opener.addheaders = [('User-agent', 'Wikitin')]+headers
-#        return opener.open(url)
-#
-#    def _fetch_url(self, url, headers=[]):
-#        return self._open_url(url, headers).read()
+            if showCategories and len(keys)>0:
+                category = self.feedItems.append(None, (None, title, categoryId))
+                #print "catID" + str(categoryId) + " " + str(self.category)
+                if categoryId == self.category:
+                    #print categoryId
+                    expandedRow = category
+    
+            for key in keys:
+                unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
+                title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
+                updateTime = self.listing.getFeedUpdateTime(key)
+                subtitle = '%s / %d unread items' % (updateTime, unreadItems)
+                if unreadItems:
+                    markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
+                else:
+                    markup = FEED_TEMPLATE % (title, subtitle)
         
+                try:
+                    icon_filename = self.listing.getFavicon(key)
+                    pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
+                                                   LIST_ICON_SIZE, LIST_ICON_SIZE)
+                except:
+                    pixbuf = default_pixbuf
+                
+                if showCategories:
+                    self.feedItems.append(category, (pixbuf, markup, key))
+                else:
+                    self.feedItems.append(None, (pixbuf, markup, key))
+                    
+                
+        self.restoreExpandedLines()
+        #try:
+            
+        #    self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
+        #except:
+        #    pass
+
+    def on_feedList_row_activated(self, treeview, path, column):
+        model = treeview.get_model()
+        iter = model.get_iter(path)
+        key = model.get_value(iter, COLUMN_KEY)
         
-    def button_clicked(widget, button, app, window, key, index):
-        app.displayArticle(key, index)
-    
-    def buttonFeedClicked(widget, button, app, window, key):
-        app.displayFeed(key)
-     
+        try:
+            #print "Key: " + str(key)
+            catId = int(key)
+            self.category = catId
+            if treeview.row_expanded(path):
+                treeview.collapse_row(path)
+        #else:
+        #    treeview.expand_row(path, True)
+            #treeview.collapse_all()
+            #treeview.expand_row(path, False)
+            #for i in range(len(path)):
+            #    self.feedList.expand_row(path[:i+1], False)
+            #self.show_confirmation_note(self.window, "Working")
+            #return True
+        except:
+            if key:
+                self.openFeed(key)
+            
+    def openFeed(self, key):
+        if key != None:
+            self.build_feed_display()
+            self.disp.displayFeed(key)
+                
+    def openArticle(self, key, id):
+        if key != None:
+            self.openFeed(key)
+            self.disp.button_clicked(None, id)
+
+    def onFeedClosed(self, object, key):
+        gobject.idle_add(self.displayListing)
+        
+    def quit(self, *args):
+        self.window.hide()
+        gtk.main_quit ()
+
     def run(self):
-        self.window.connect("destroy", gtk.main_quit)
-        #self.window.show_all()
+        self.window.connect("destroy", self.quit)
         gtk.main()
 
+    def prefsClosed(self, *widget):
+        try:
+            self.orientation.set_mode(self.config.getOrientation())
+        except:
+            pass
+        self.displayListing()
+        self.checkAutoUpdate()
+
+    def checkAutoUpdate(self, *widget):
+        interval = int(self.config.getUpdateInterval()*3600000)
+        if self.config.isAutoUpdateEnabled():
+            if self.autoupdate == False:
+                self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
+                self.autoupdate = interval
+            elif not self.autoupdate == interval:
+                # If auto-update is enabled, but not at the right frequency
+                gobject.source_remove(self.autoupdateId)
+                self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
+                self.autoupdate = interval
+        else:
+            if not self.autoupdate == False:
+                gobject.source_remove(self.autoupdateId)
+                self.autoupdate = False
+
+    def automaticUpdate(self, *widget):
+        # Need to check for internet connection
+        # If no internet connection, try again in 10 minutes:
+        # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
+        #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
+        #from time import localtime, strftime
+        #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
+        #file.close()
+        self.button_update_clicked(None, None)
+        return True
+    
+    def getStatus(self):
+        status = ""
+        for key in self.listing.getListOfFeeds():
+            if self.listing.getFeedNumberOfUnreadItems(key) > 0:
+                status += self.listing.getFeedTitle(key) + ": \t" +  str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
+        if status == "":
+            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("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"
-            sys.exit(1)
+            logger.error("Error: Can't create configuration directory")
+            from sys import exit
+            exit(1)
     app = FeedingIt()
     app.run()