X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2FFeedingIt.py;h=29dc7fa20342471aac86135b193da36941a8fb9e;hb=95b2b55820745606cc7dae2df4fbbb214eeb161c;hp=62960eb446445ae7b8fb6f4f8a3ce05bff2bccc4;hpb=ef0735a803118523dc0e2a621e1b45a1e1644ed6;p=feedingit diff --git a/src/FeedingIt.py b/src/FeedingIt.py index 62960eb..29dc7fa 100644 --- a/src/FeedingIt.py +++ b/src/FeedingIt.py @@ -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 @@ -19,12 +20,11 @@ # ============================================================================ __appname__ = 'FeedingIt' __author__ = 'Yves Marcoz' -__version__ = '0.7.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 @@ -35,26 +35,31 @@ 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 import Listing +from rss_sqlite import Listing from opml import GetOpmlData, ExportOpmlData -from urllib2 import install_opener, build_opener +import mainthread from socket import setdefaulttimeout timeout = 5 setdefaulttimeout(timeout) del timeout +import xml.sax + LIST_ICON_SIZE = 32 LIST_ICON_BORDER = 10 @@ -90,10 +95,18 @@ MARKUP_TEMPLATE_ENTRY = '%%s0: - # In preparation for i18n/l10n - def N_(a, b, n): - return (a if n == 1 else b) - - self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total) - - self.fraction = 0 - self.set_fraction(self.fraction) - self.show_all() - # Create a timeout - self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar) - - def update_progress_bar(self): - #self.progress_bar.pulse() - if activeCount() < 4: - x = activeCount() - 1 - k = len(self.listOfKeys) - fin = self.total - k - x - fraction = float(fin)/float(self.total) + float(x)/(self.total*2.) - #print x, k, fin, fraction - self.set_fraction(fraction) - - if len(self.listOfKeys)>0: - self.current = self.current+1 - key = self.listOfKeys.pop() - #if self.single == True: - # Check if the feed is being displayed - download = Download(self.listing, key, self.config) - download.start() - return True - elif activeCount() > 1: - return True + @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: - #self.waitingWindow.destroy() - #self.destroy() - try: - del self.update_lock - except: - pass - self.emit("download-done", "success") - return False - return True - - + bar.set_text(text) + bar.set_fraction(cls.progress) + class SortList(hildon.StackableWindow): - def __init__(self, parent, listing, feedingit, after_closing): + def __init__(self, parent, listing, feedingit, after_closing, category=None): hildon.StackableWindow.__init__(self) self.set_transient_for(parent) - self.set_title('Subscriptions') + 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 - self.connect('destroy', lambda w: self.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) @@ -368,10 +504,16 @@ class SortList(hildon.StackableWindow): #rect = self.treeview.get_visible_rect() #y = rect.y+rect.height self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - for key in self.listing.getListOfFeeds(): - item = self.treestore.append([self.listing.getFeedTitle(key), key]) - if key == selected: - selectedItem = item + 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) @@ -400,29 +542,34 @@ class SortList(hildon.StackableWindow): def buttonUp(self, button): key = self.getSelectedItem() if not key == None: - self.listing.moveUp(key) + 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: - self.listing.moveDown(key) + if self.isEditingCategories: + self.listing.moveCategoryDown(key) + else: + self.listing.moveDown(key) self.refreshList(key, 10) def buttonDelete(self, button): key = self.getSelectedItem() - if key == 'ArchivedArticles': - message = 'Cannot remove the archived articles feed.' - hildon.hildon_banner_show_information(self, '', message) - elif key is not None: - 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: + 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() + self.refreshList() def buttonEdit(self, button): key = self.getSelectedItem() @@ -431,130 +578,192 @@ class SortList(hildon.StackableWindow): message = 'Cannot edit the archived articles feed.' hildon.hildon_banner_show_information(self, '', message) return - - if key is not None: - wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True) - ret = wizard.run() - if ret == 2: - (title, url) = wizard.getData() - if (not title == '') and (not url == ''): - self.listing.editFeed(key, title, url) - self.refreshList() - wizard.destroy() + 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://"): - wizard = AddWidgetWizard(self, urlIn) - ret = wizard.run() - if ret == 2: - (title, url) = wizard.getData() - if (not title == '') and (not url == ''): - self.listing.addFeed(title, url) + 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): - 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": - if contentLink.startswith("/home/user/"): - self.view.open("file://" + contentLink) - else: - self.view.load_html_string('This article has not been downloaded yet. Click here to view online.' % contentLink, 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) - #else: - #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link) - self.view.set_zoom_level(float(config.getArtFontSize())/10.) - #else: - # if not key == "ArchivedArticles": - # Do not download images if the feed is "Archived Articles" - # self.document.connect("request-url", self._signal_request_url) - - # self.document.clear() - # self.document.open_stream("text/html") - # self.document.write_stream(self.text) - # self.document.close_stream() - + 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._signal_link_clicked, self.feed.getExternalLink(self.id)) - 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.event_box = gtk.EventBox() - #self.event_box.add(self.pannable_article) - self.add(self.pannable_article) - - - self.pannable_article.show_all() - self.destroyId = self.connect("destroy", self.destroyWindow) - - self.view.connect("button_press_event", self.button_pressed) - self.gestureId = self.view.connect("button_release_event", self.button_released) - #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle) + + 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 here 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): - #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): @@ -563,27 +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) - #print x, y - #print "Released" - - #def gesture(self, widget, direction, startx, starty): - # if (direction == 3): - # self.emit("article-next", self.index) - # if (direction == 2): - # self.emit("article-previous", self.index) - #print startx, starty - #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow) + 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): @@ -592,48 +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 reloadArticle(self, *widget): - # if threading.activeCount() > 1: - # Image thread are still running, come back in a bit - # return True - # else: - # for (stream, imageThread) in self.images: - # imageThread.join() - # stream.write(imageThread.data) - # stream.close() - # return False - # self.show_all() - - def _signal_link_clicked(self, object, link): - 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') - iface.open_new_window(link) - #def _signal_request_url(self, object, url, stream): - #print url - # self.imageDownloader.queueImage(url, stream) - #imageThread = GetImage(url) - #imageThread.start() - #self.images.append((stream, imageThread)) + 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, 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.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 @@ -651,20 +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.displayFeed() + 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.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: @@ -679,93 +921,107 @@ 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) - gobject.idle_add(self.feed.saveUnread, CONFIGDIR) - self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems()) - 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("","").replace("","").replace("","").replace("","").replace("","")) - def displayFeed(self): - 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) - 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.feed = self.listing.getFeed(key) + self.feedTitle = self.listing.getFeedTitle(key) + self.key = key - - 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) + self.set_title(self.feedTitle) - vbox= gtk.VBox(False, 10) - vbox.pack_start(self.feedList) - - 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', "#333333") - (width, height) = self.get_size() - self.markup_renderer.set_property('wrap-width', width-20) - self.markup_renderer.set_property('ypad', 5) - self.markup_renderer.set_property('xpad', 5) - markup_column = gtk.TreeViewColumn('', self.markup_renderer, \ - markup=FEED_COLUMN_MARKUP) - self.feedList.append_column(markup_column) + 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() - hasArticle = False - for id in self.feed.getIds(): - isRead = False + 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: - pass + except Exception: + isRead = False if not ( isRead and hideReadArticles ): - #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ): - #title = self.feed.getTitle(id) title = self.fix_title(self.feed.getTitle(id)) - - #if self.feed.isEntryRead(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.add(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) @@ -777,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() @@ -800,14 +1055,11 @@ 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() self.feed.purgeReadArticles() - self.feed.saveUnread(CONFIGDIR) - self.feed.saveFeed(CONFIGDIR) + #self.feed.saveFeed(CONFIGDIR) self.displayFeed() def destroyArticle(self, handle): @@ -824,44 +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) - if self.config.getHideReadArticles(): - isRead = False - try: - isRead = self.feed.isEntryRead(id) - except: - pass - while isRead and id != index: - id = self.feed.getNextId(id) - isRead = False - try: - isRead = self.feed.isEntryRead(id) - except: - pass - 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) - if self.config.getHideReadArticles(): - isRead = False - try: - isRead = self.feed.isEntryRead(id) - except: - pass - while isRead and id != index: - id = self.feed.getPreviousId(id) - isRead = False - try: - isRead = self.feed.isEntryRead(id) - except: - pass - 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) @@ -870,66 +1084,135 @@ class DisplayFeed(hildon.StackableWindow): def onArticleDeleted(self, object, index): self.clear() self.feed.removeArticle(index) - self.feed.saveUnread(CONFIGDIR) - self.feed.saveFeed(CONFIGDIR) + #self.feed.saveFeed(CONFIGDIR) self.displayFeed() + + def do_update_feed(self): + self.listing.updateFeed (self.key, priority=-1) + def button_update_clicked(self, button): - #bar = DownloadBar(self, self.listing, [self.key,], self.config ) - if not type(self.downloadDialog).__name__=="DownloadBar": - self.pannableFeed.destroy() - self.vbox = gtk.VBox(False, 10) - self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True ) - self.downloadDialog.connect("download-done", self.onDownloadsDone) - self.vbox.pack_start(self.downloadDialog, expand=False, fill=False) - self.add(self.vbox) - self.show_all() + gobject.idle_add(self.do_update_feed) - def onDownloadsDone(self, *widget): - self.vbox.destroy() - self.feed = self.listing.getFeed(self.key) - self.displayFeed() - self.updateDbusHandler.ArticleCountUpdated() - + 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): - for index in self.feed.getIds(): - self.feed.setEntryRead(index) - self.mark_item_read(index) + #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() - 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.introLabel = gtk.Label("Loading...") + 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() - self.config = Config(self.window, CONFIGDIR+"config.ini") gobject.idle_add(self.createWindow) - + def createWindow(self): - self.app_lock = get_lock("app_lock") - if self.app_lock == None: - self.introLabel.set_label("Update in progress, please wait.") - gobject.timeout_add_seconds(3, self.createWindow) - return False - self.listing = Listing(CONFIGDIR) - + self.category = 0 + 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) @@ -941,7 +1224,12 @@ class FeedingIt: 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) @@ -959,46 +1247,69 @@ 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.ListStore(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() + # 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.enableDbus) - - def enableDbus(self): - self.dbusHandler = ServerObject(self) - self.updateDbusHandler = UpdateServerObject(self) + + 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) - for id in feed.getIds(): - feed.setEntryRead(id) - feed.saveUnread(CONFIGDIR) - self.listing.updateUnread(key, feed.getNumberOfUnreadItems()) + feed.markAllAsRead() + #for id in feed.getIds(): + # feed.setEntryRead(id) + self.listing.updateUnread(key) self.displayListing() def button_about_clicked(self, button): @@ -1023,36 +1334,31 @@ class FeedingIt: self.displayListing() def addFeed(self, urlIn="http://"): - wizard = AddWidgetWizard(self.window, urlIn) + 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.listing.saveConfig() 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): - if not type(self.downloadDialog).__name__=="DownloadBar": - self.updateDbusHandler.UpdateStarted() - self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config ) - self.downloadDialog.connect("download-done", self.onDownloadsDone) - self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False) - self.mainVbox.show_all() - #self.displayListing() + 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() @@ -1069,69 +1375,125 @@ class FeedingIt: 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): icon_theme = gtk.icon_theme_get_default() default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \ gtk.ICON_LOOKUP_USE_BUILTIN) + self.saveExpandedLines() + self.feedItems.clear() - for key in self.listing.getListOfFeeds(): - unreadItems = self.listing.getFeedNumberOfUnreadItems(key) - if unreadItems > 0 or not self.config.getHideReadFeeds(): - title = self.listing.getFeedTitle(key) + hideReadFeed = self.config.getHideReadFeeds() + order = self.config.getFeedSortOrder() + + categories = self.listing.getListOfCategories() + if len(categories) > 1: + showCategories = True + else: + showCategories = False + + for categoryId in categories: + + title = self.listing.getCategoryTitle(categoryId) + keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId) + + 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) + LIST_ICON_SIZE, LIST_ICON_SIZE) except: pixbuf = default_pixbuf - - self.feedItems.append((pixbuf, markup, key)) + + 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) - self.openFeed(key) - - def openFeed(self, key): + try: - self.feed_lock + #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 feed_lock doesn't exist, we can open the feed, else we do nothing - 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: + 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): - #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): - self.listing.saveConfig() - del self.feed_lock - self.updateDbusHandler.ArticleCountUpdated() - + def quit(self, *args): + self.window.hide() + gtk.main_quit () + def run(self): - self.window.connect("destroy", gtk.main_quit) + self.window.connect("destroy", self.quit) gtk.main() - self.listing.saveConfig() - del self.app_lock def prefsClosed(self, *widget): try: @@ -1168,13 +1530,6 @@ class FeedingIt: self.button_update_clicked(None, None) return True - def stopUpdate(self): - # Not implemented in the app (see update_feeds.py) - try: - self.downloadDialog.listOfKeys = [] - except: - pass - def getStatus(self): status = "" for key in self.listing.getListOfFeeds(): @@ -1184,19 +1539,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()