1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # ============================================================================
21 __appname__ = 'FeedingIt'
22 __author__ = 'Yves Marcoz'
23 __version__ = '0.9.1~woodchuck'
24 __description__ = 'A simple RSS Reader for Maemo 5'
25 # ============================================================================
32 from webkit import WebView
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat, environ
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from feedingitdbus import ServerObject
43 from config import Config
44 from cgi import escape
49 logger = logging.getLogger(__name__)
51 from rss_sqlite import Listing
52 from opml import GetOpmlData, ExportOpmlData
56 from socket import setdefaulttimeout
58 setdefaulttimeout(timeout)
66 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
67 ABOUT_ICON = 'feedingit'
68 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
69 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
70 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
71 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
73 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
74 unread_color = color_style.lookup_color('ActiveTextColor')
75 read_color = color_style.lookup_color('DefaultTextColor')
78 CONFIGDIR="/home/user/.feedingit/"
79 LOCK = CONFIGDIR + "update.lock"
82 from htmlentitydefs import name2codepoint
84 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
86 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
90 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
91 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
92 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
94 # Build the markup template for the Maemo 5 text style
95 head_font = style.get_font_desc('SystemFont')
96 sub_font = style.get_font_desc('SmallSystemFont')
98 #head_color = style.get_color('ButtonTextColor')
99 head_color = style.get_color('DefaultTextColor')
100 sub_color = style.get_color('DefaultTextColor')
101 active_color = style.get_color('ActiveTextColor')
103 bg_color = style.get_color('DefaultBackgroundColor').to_string()
104 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
105 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
106 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
107 bg_color = "#" + c1 + c2 + c3
110 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
111 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
113 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
114 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
116 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
117 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
119 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
120 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
122 FEED_TEMPLATE = '\n'.join((head, normal_sub))
123 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
125 ENTRY_TEMPLATE = entry_head
126 ENTRY_TEMPLATE_UNREAD = entry_active_head
128 notification_iface = None
131 global notification_iface
133 bus = dbus.SessionBus()
134 proxy = bus.get_object('org.freedesktop.Notifications',
135 '/org/freedesktop/Notifications')
137 = dbus.Interface(proxy, 'org.freedesktop.Notifications')
140 notification_iface.SystemNoteInfoprint("FeedingIt: " + message)
142 if notification_iface is None:
147 except dbus.DBusException:
148 # Rebind the name and try again.
153 # Removes HTML or XML character references and entities from a text string.
155 # @param text The HTML (or XML) source text.
156 # @return The plain text, as a Unicode string, if necessary.
157 # http://effbot.org/zone/re-sub.htm#unescape-html
162 # character reference
164 if text[:3] == "&#x":
165 return unichr(int(text[3:-1], 16))
167 return unichr(int(text[2:-1]))
173 text = unichr(name2codepoint[text[1:-1]])
176 return text # leave as is
177 return sub("&#?\w+;", fixup, text)
180 class AddWidgetWizard(gtk.Dialog):
181 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
182 gtk.Dialog.__init__(self)
183 self.set_transient_for(parent)
185 #self.category = categories[0]
186 self.category = currentCat
189 self.set_title('Edit RSS feed')
191 self.set_title('Add new RSS feed')
194 self.btn_add = self.add_button('Save', 2)
196 self.btn_add = self.add_button('Add', 2)
198 self.set_default_response(2)
200 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
201 self.nameEntry.set_placeholder('Feed name')
202 # If titleIn matches urlIn, there is no title.
203 if not titleIn == None and titleIn != urlIn:
204 self.nameEntry.set_text(titleIn)
205 self.nameEntry.select_region(-1, -1)
207 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
208 self.urlEntry.set_placeholder('Feed URL')
209 self.urlEntry.set_text(urlIn)
210 self.urlEntry.select_region(-1, -1)
211 self.urlEntry.set_activates_default(True)
213 self.table = gtk.Table(3, 2, False)
214 self.table.set_col_spacings(5)
215 label = gtk.Label('Name:')
216 label.set_alignment(1., .5)
217 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
218 self.table.attach(self.nameEntry, 1, 2, 0, 1)
219 label = gtk.Label('URL:')
220 label.set_alignment(1., .5)
221 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
222 self.table.attach(self.urlEntry, 1, 2, 1, 2)
223 selector = self.create_selector(categories, listing)
224 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
225 picker.set_selector(selector)
226 picker.set_title("Select category")
227 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
228 picker.set_name('HildonButton-finger')
229 picker.set_alignment(0,0,1,1)
231 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
233 self.vbox.pack_start(self.table)
238 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
240 def create_selector(self, choices, listing):
241 #self.pickerDialog = hildon.PickerDialog(self.parent)
242 selector = hildon.TouchSelector(text=True)
246 title = listing.getCategoryTitle(item)
247 iter = selector.append_text(str(title))
248 if self.category == item:
249 selector.set_active(0, index)
250 self.map[title] = item
252 selector.connect("changed", self.selection_changed)
253 #self.pickerDialog.set_selector(selector)
256 def selection_changed(self, selector, button):
257 current_selection = selector.get_current_text()
258 if current_selection:
259 self.category = self.map[current_selection]
261 class AddCategoryWizard(gtk.Dialog):
262 def __init__(self, parent, titleIn=None, isEdit=False):
263 gtk.Dialog.__init__(self)
264 self.set_transient_for(parent)
267 self.set_title('Edit Category')
269 self.set_title('Add Category')
272 self.btn_add = self.add_button('Save', 2)
274 self.btn_add = self.add_button('Add', 2)
276 self.set_default_response(2)
278 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
279 self.nameEntry.set_placeholder('Category name')
280 if not titleIn == None:
281 self.nameEntry.set_text(titleIn)
282 self.nameEntry.select_region(-1, -1)
284 self.table = gtk.Table(1, 2, False)
285 self.table.set_col_spacings(5)
286 label = gtk.Label('Name:')
287 label.set_alignment(1., .5)
288 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
289 self.table.attach(self.nameEntry, 1, 2, 0, 1)
290 #label = gtk.Label('URL:')
291 #label.set_alignment(1., .5)
292 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
293 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
294 self.vbox.pack_start(self.table)
299 return self.nameEntry.get_text()
301 class DownloadBar(gtk.ProgressBar):
304 if hasattr (cls, 'class_init_done'):
307 cls.downloadbars = []
308 # Total number of jobs we are monitoring.
310 # Number of jobs complete (of those that we are monitoring).
315 cls.class_init_done = True
317 bus = dbus.SessionBus()
318 bus.add_signal_receiver(handler_function=cls.update_progress,
320 signal_name='UpdateProgress',
321 dbus_interface='org.marcoz.feedingit',
322 path='/org/marcoz/feedingit/update')
324 def __init__(self, parent):
327 gtk.ProgressBar.__init__(self)
329 self.downloadbars.append(weakref.ref (self))
331 self.__class__.update_bars()
334 def downloading(cls):
336 return cls.done != cls.total
339 def update_progress(cls, percent_complete,
340 completed, in_progress, queued,
341 bytes_downloaded, bytes_updated, bytes_per_second,
343 if not cls.downloadbars:
346 cls.total = completed + in_progress + queued
348 cls.progress = percent_complete / 100.
349 if cls.progress < 0: cls.progress = 0
350 if cls.progress > 1: cls.progress = 1
353 for ref in cls.downloadbars:
356 # The download bar disappeared.
357 cls.downloadbars.remove (ref)
359 bar.emit("download-done", feed_updated)
361 if in_progress == 0 and queued == 0:
362 for ref in cls.downloadbars:
365 # The download bar disappeared.
366 cls.downloadbars.remove (ref)
368 bar.emit("download-done", None)
374 def update_bars(cls):
375 # In preparation for i18n/l10n
377 return (a if n == 1 else b)
379 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
381 % (cls.done, cls.total))
383 for ref in cls.downloadbars:
386 # The download bar disappeared.
387 cls.downloadbars.remove (ref)
390 bar.set_fraction(cls.progress)
392 class SortList(hildon.StackableWindow):
393 def __init__(self, parent, listing, feedingit, after_closing, category=None):
394 hildon.StackableWindow.__init__(self)
395 self.set_transient_for(parent)
397 self.isEditingCategories = False
398 self.category = category
399 self.set_title(listing.getCategoryTitle(category))
401 self.isEditingCategories = True
402 self.set_title('Categories')
403 self.listing = listing
404 self.feedingit = feedingit
405 self.after_closing = after_closing
407 self.connect('destroy', lambda w: self.after_closing())
408 self.vbox2 = gtk.VBox(False, 2)
410 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
411 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
412 button.connect("clicked", self.buttonUp)
413 self.vbox2.pack_start(button, expand=False, fill=False)
415 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
416 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
417 button.connect("clicked", self.buttonDown)
418 self.vbox2.pack_start(button, expand=False, fill=False)
420 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
422 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
423 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
424 button.connect("clicked", self.buttonAdd)
425 self.vbox2.pack_start(button, expand=False, fill=False)
427 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
428 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
429 button.connect("clicked", self.buttonEdit)
430 self.vbox2.pack_start(button, expand=False, fill=False)
432 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
433 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
434 button.connect("clicked", self.buttonDelete)
435 self.vbox2.pack_start(button, expand=False, fill=False)
437 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
438 #button.set_label("Done")
439 #button.connect("clicked", self.buttonDone)
440 #self.vbox.pack_start(button)
441 self.hbox2= gtk.HBox(False, 10)
442 self.pannableArea = hildon.PannableArea()
443 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
444 self.treeview = gtk.TreeView(self.treestore)
445 self.hbox2.pack_start(self.pannableArea, expand=True)
447 self.hbox2.pack_end(self.vbox2, expand=False)
448 self.set_default_size(-1, 600)
451 menu = hildon.AppMenu()
452 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
453 button.set_label("Import from OPML")
454 button.connect("clicked", self.feedingit.button_import_clicked)
457 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
458 button.set_label("Export to OPML")
459 button.connect("clicked", self.feedingit.button_export_clicked)
461 self.set_app_menu(menu)
465 #self.connect("destroy", self.buttonDone)
467 def displayFeeds(self):
468 self.treeview.destroy()
469 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
470 self.treeview = gtk.TreeView()
472 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
473 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
475 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
477 self.pannableArea.add(self.treeview)
481 def refreshList(self, selected=None, offset=0):
482 #rect = self.treeview.get_visible_rect()
483 #y = rect.y+rect.height
484 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
485 if self.isEditingCategories:
486 for key in self.listing.getListOfCategories():
487 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
491 for key in self.listing.getListOfFeeds(category=self.category):
492 item = self.treestore.append([self.listing.getFeedTitle(key), key])
495 self.treeview.set_model(self.treestore)
496 if not selected == None:
497 self.treeview.get_selection().select_iter(selectedItem)
498 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
499 self.pannableArea.show_all()
501 def getSelectedItem(self):
502 (model, iter) = self.treeview.get_selection().get_selected()
505 return model.get_value(iter, 1)
507 def findIndex(self, key):
511 for row in self.treestore:
513 return (before, row.iter)
514 if key == list(row)[0]:
518 return (before, None)
520 def buttonUp(self, button):
521 key = self.getSelectedItem()
523 if self.isEditingCategories:
524 self.listing.moveCategoryUp(key)
526 self.listing.moveUp(key)
527 self.refreshList(key, -10)
529 def buttonDown(self, button):
530 key = self.getSelectedItem()
532 if self.isEditingCategories:
533 self.listing.moveCategoryDown(key)
535 self.listing.moveDown(key)
536 self.refreshList(key, 10)
538 def buttonDelete(self, button):
539 key = self.getSelectedItem()
541 message = 'Really remove this feed and its entries?'
542 dlg = hildon.hildon_note_new_confirmation(self, message)
545 if response == gtk.RESPONSE_OK:
546 if self.isEditingCategories:
547 self.listing.removeCategory(key)
549 self.listing.removeFeed(key)
552 def buttonEdit(self, button):
553 key = self.getSelectedItem()
555 if key == 'ArchivedArticles':
556 message = 'Cannot edit the archived articles feed.'
557 hildon.hildon_banner_show_information(self, '', message)
559 if self.isEditingCategories:
561 SortList(self.parent, self.listing, self.feedingit, None, category=key)
564 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
567 (title, url, category) = wizard.getData()
569 self.listing.editFeed(key, title, url, category=category)
573 def buttonDone(self, *args):
576 def buttonAdd(self, button, urlIn="http://"):
577 if self.isEditingCategories:
578 wizard = AddCategoryWizard(self)
581 title = wizard.getData()
582 if (not title == ''):
583 self.listing.addCategory(title)
585 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
588 (title, url, category) = wizard.getData()
590 self.listing.addFeed(title, url, category=category)
595 class DisplayArticle(hildon.StackableWindow):
597 A Widget for displaying an article.
599 def __init__(self, article_id, feed, feed_key, articles, config, listing):
601 article_id - The identifier of the article to load.
603 feed - The feed object containing the article (an
604 rss_sqlite:Feed object).
606 feed_key - The feed's identifier.
608 articles - A list of articles from the feed to display.
609 Needed for selecting the next/previous article (article_next).
611 config - A configuration object (config:Config).
613 listing - The listing object (rss_sqlite:Listing) that
614 contains the feed and article.
616 hildon.StackableWindow.__init__(self)
618 self.article_id = None
620 self.feed_key = feed_key
621 self.articles = articles
623 self.listing = listing
625 self.set_title(self.listing.getFeedTitle(feed_key))
627 # Init the article display
628 self.view = WebView()
629 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
630 self.view.connect("motion-notify-event", lambda w,ev: True)
631 self.view.connect('load-started', self.load_started)
632 self.view.connect('load-finished', self.load_finished)
633 self.view.connect('navigation-requested', self.navigation_requested)
634 self.view.connect("button_press_event", self.button_pressed)
635 self.gestureId = self.view.connect(
636 "button_release_event", self.button_released)
638 self.pannable_article = hildon.PannableArea()
639 self.pannable_article.add(self.view)
641 self.add(self.pannable_article)
643 self.pannable_article.show_all()
646 menu = hildon.AppMenu()
648 def menu_button(label, callback):
649 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
650 button.set_label(label)
651 button.connect("clicked", callback)
654 menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
655 menu_button("Open in browser", self.open_in_browser)
656 if feed_key == "ArchivedArticles":
658 "Remove from archived articles", self.remove_archive_button)
660 menu_button("Add to archived articles", self.archive_button)
662 self.set_app_menu(menu)
665 self.destroyId = self.connect("destroy", self.destroyWindow)
667 self.article_open(article_id)
669 def article_open(self, article_id):
671 Load the article with the specified id.
673 # If an article was open, close it.
674 if self.article_id is not None:
675 self.article_closed()
677 self.article_id = article_id
678 self.set_for_removal = False
679 self.loadedArticle = False
680 self.initial_article_load = True
682 contentLink = self.feed.getContentLink(self.article_id)
683 if contentLink.startswith("/home/user/"):
684 self.view.open("file://%s" % contentLink)
685 self.currentUrl = self.feed.getExternalLink(self.article_id)
687 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
688 self.currentUrl = str(contentLink)
690 self.feed.setEntryRead(self.article_id)
692 def article_closed(self):
694 The user has navigated away from the article. Execute any
697 if self.set_for_removal:
698 self.emit("article-deleted", self.article_id)
700 self.emit("article-closed", self.article_id)
703 def navigation_requested(self, wv, fr, req):
705 http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
708 fr - a WebKitWebFrame
709 req - WebKitNetworkRequest
711 if self.initial_article_load:
712 # Always initially load an article in the internal
714 self.initial_article_load = False
717 # When following a link, only use the internal browser if so
718 # configured. Otherwise, launch an external browser.
719 if self.config.getOpenInExternalBrowser():
720 self.open_in_browser(None, req.get_uri())
725 def load_started(self, *widget):
726 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
728 def load_finished(self, *widget):
729 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
730 frame = self.view.get_main_frame()
731 if self.loadedArticle:
732 self.currentUrl = frame.get_uri()
734 self.loadedArticle = True
736 def button_pressed(self, window, event):
738 The user pressed a "mouse button" (in our case, this means the
739 user likely started to drag with the finger).
741 We are only interested in whether the user performs a drag.
742 We record the starting position and when the user "releases
743 the button," we see how far the mouse moved.
745 self.coords = (event.x, event.y)
747 def button_released(self, window, event):
748 x = self.coords[0] - event.x
749 y = self.coords[1] - event.y
751 if (2*abs(y) < abs(x)):
753 self.article_next(forward=False)
755 self.article_next(forward=True)
757 # We handled the event. Don't propagate it further.
760 def article_next(self, forward=True):
762 Advance to the next (or, if forward is false, the previous)
770 id = self.feed.getNextId(id, forward)
778 if id in self.articles:
779 self.article_open(id)
782 def destroyWindow(self, *args):
783 self.article_closed()
784 self.disconnect(self.destroyId)
787 def horiz_scrolling_button(self, *widget):
788 self.pannable_article.disconnect(self.gestureId)
789 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
791 def archive_button(self, *widget):
792 # Call the listing.addArchivedArticle
793 self.listing.addArchivedArticle(self.feed_key, self.article_id)
795 def remove_archive_button(self, *widget):
796 self.set_for_removal = True
798 def open_in_browser(self, object, link=None):
800 Open the specified link using the system's browser. If not
801 link is specified, reopen the current page using the system's
805 link = self.currentUrl
807 bus = dbus.SessionBus()
808 b_proxy = bus.get_object("com.nokia.osso_browser",
809 "/com/nokia/osso_browser/request")
810 b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
812 notify("Opening %s" % link)
814 # We open the link asynchronously: if the web browser is not
815 # already running, this can take a while.
818 Something went wrong opening the URL.
821 notify("Error opening %s: %s" % (link, str(exception)))
824 b_iface.open_new_window(link,
825 reply_handler=lambda *args: None,
826 error_handler=error_handler())
828 class DisplayFeed(hildon.StackableWindow):
829 def __init__(self, listing, config):
830 hildon.StackableWindow.__init__(self)
831 self.connect('configure-event', self.on_configure_event)
832 self.connect("delete_event", self.delete_window)
833 self.connect("destroy", self.destroyWindow)
835 self.listing = listing
840 # If hide read articles is set, this is set to the set of
841 # unread articles at the time that feed is loaded. The last
842 # bit is important: when the user selects the next article,
843 # but then decides to move back, previous should select the
845 self.articles = list()
847 self.downloadDialog = False
849 #self.listing.setCurrentlyDisplayedFeed(self.key)
853 menu = hildon.AppMenu()
854 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
855 button.set_label("Update feed")
856 button.connect("clicked", self.button_update_clicked)
859 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
860 button.set_label("Mark all as read")
861 button.connect("clicked", self.buttonReadAllClicked)
864 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
865 button.set_label("Delete read articles")
866 button.connect("clicked", self.buttonPurgeArticles)
868 self.archived_article_buttons = [button]
870 self.set_app_menu(menu)
873 self.feedItems = gtk.ListStore(str, str)
875 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
876 self.feedList.set_model(self.feedItems)
877 self.feedList.set_rules_hint(True)
878 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
879 self.feedList.set_hover_selection(False)
880 self.feedList.connect('hildon-row-tapped',
881 self.on_feedList_row_activated)
883 self.markup_renderer = gtk.CellRendererText()
884 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
885 self.markup_renderer.set_property('background', bg_color) #"#333333")
886 (width, height) = self.get_size()
887 self.markup_renderer.set_property('wrap-width', width-20)
888 self.markup_renderer.set_property('ypad', 8)
889 self.markup_renderer.set_property('xpad', 5)
890 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
891 markup=FEED_COLUMN_MARKUP)
892 self.feedList.append_column(markup_column)
894 vbox = gtk.VBox(False, 10)
895 vbox.pack_start(self.feedList)
897 self.pannableFeed = hildon.PannableArea()
898 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
899 self.pannableFeed.add_with_viewport(vbox)
901 self.main_vbox = gtk.VBox(False, 0)
902 self.main_vbox.pack_start(self.pannableFeed)
904 self.add(self.main_vbox)
906 self.main_vbox.show_all()
908 def on_configure_event(self, window, event):
909 if getattr(self, 'markup_renderer', None) is None:
912 # Fix up the column width for wrapping the text when the window is
913 # resized (i.e. orientation changed)
914 self.markup_renderer.set_property('wrap-width', event.width-20)
915 it = self.feedItems.get_iter_first()
916 while it is not None:
917 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
918 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
919 it = self.feedItems.iter_next(it)
921 def delete_window(self, *args):
923 Prevent the window from being deleted.
929 except AttributeError:
933 self.listing.updateUnread(key)
934 self.emit("feed-closed", key)
936 self.feedItems.clear()
940 def destroyWindow(self, *args):
943 except AttributeError:
948 def fix_title(self, title):
949 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
951 def displayFeed(self, key=None):
953 Select and display a feed. If feed, title and key are None,
954 reloads the current feed.
959 except AttributeError:
963 self.listing.updateUnread(self.key)
964 self.emit("feed-closed", self.key)
966 self.feed = self.listing.getFeed(key)
967 self.feedTitle = self.listing.getFeedTitle(key)
970 self.set_title(self.feedTitle)
972 selection = self.feedList.get_selection()
973 if selection is not None:
974 selection.set_mode(gtk.SELECTION_NONE)
976 for b in self.archived_article_buttons:
977 if key == "ArchivedArticles":
982 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
983 hideReadArticles = self.config.getHideReadArticles()
985 articles = self.feed.getIds(onlyUnread=True)
987 articles = self.feed.getIds()
989 self.articles[:] = []
991 self.feedItems.clear()
994 isRead = self.feed.isEntryRead(id)
997 if not ( isRead and hideReadArticles ):
998 title = self.fix_title(self.feed.getTitle(id))
999 self.articles.append(id)
1001 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1003 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
1005 self.feedItems.append((markup, id))
1008 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
1009 self.feedItems.append((markup, ""))
1014 self.pannableFeed.destroy()
1015 #self.remove(self.pannableFeed)
1017 def on_feedList_row_activated(self, treeview, path): #, column):
1018 if not self.articles:
1019 # There are not actually any articles. Ignore.
1022 selection = self.feedList.get_selection()
1023 selection.set_mode(gtk.SELECTION_SINGLE)
1024 self.feedList.get_selection().select_path(path)
1025 model = treeview.get_model()
1026 iter = model.get_iter(path)
1027 key = model.get_value(iter, FEED_COLUMN_KEY)
1028 # Emulate legacy "button_clicked" call via treeview
1029 gobject.idle_add(self.button_clicked, treeview, key)
1032 def button_clicked(self, button, index, previous=False, next=False):
1033 newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
1034 stack = hildon.WindowStack.get_default()
1037 stack.pop_and_push(1, newDisp, tmp)
1039 gobject.timeout_add(200, self.destroyArticle, tmp)
1044 if type(self.disp).__name__ == "DisplayArticle":
1045 gobject.timeout_add(200, self.destroyArticle, self.disp)
1049 self.disp.show_all()
1052 if self.key == "ArchivedArticles":
1053 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
1054 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
1056 def buttonPurgeArticles(self, *widget):
1058 self.feed.purgeReadArticles()
1059 #self.feed.saveFeed(CONFIGDIR)
1062 def destroyArticle(self, handle):
1063 handle.destroyWindow()
1065 def mark_item_read(self, key):
1066 it = self.feedItems.get_iter_first()
1067 while it is not None:
1068 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1070 title = self.fix_title(self.feed.getTitle(key))
1071 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1072 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1074 it = self.feedItems.iter_next(it)
1076 def onArticleClosed(self, object, index):
1077 selection = self.feedList.get_selection()
1078 selection.set_mode(gtk.SELECTION_NONE)
1079 self.mark_item_read(index)
1081 def onArticleDeleted(self, object, index):
1083 self.feed.removeArticle(index)
1084 #self.feed.saveFeed(CONFIGDIR)
1088 def do_update_feed(self):
1089 self.listing.updateFeed (self.key, priority=-1)
1091 def button_update_clicked(self, button):
1092 gobject.idle_add(self.do_update_feed)
1094 def show_download_bar(self):
1095 if not type(self.downloadDialog).__name__=="DownloadBar":
1096 self.downloadDialog = DownloadBar(self.window)
1097 self.downloadDialog.connect("download-done", self.onDownloadDone)
1098 self.main_vbox.pack_end(self.downloadDialog,
1099 expand=False, fill=False)
1100 self.downloadDialog.show()
1102 def onDownloadDone(self, widget, feed):
1103 if feed is not None and hasattr(self, 'feed') and feed == self.feed:
1104 self.feed = self.listing.getFeed(self.key)
1108 self.downloadDialog.destroy()
1109 self.downloadDialog = False
1111 def buttonReadAllClicked(self, button):
1113 self.feed.markAllAsRead()
1114 it = self.feedItems.get_iter_first()
1115 while it is not None:
1116 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1117 title = self.fix_title(self.feed.getTitle(k))
1118 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1119 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1120 it = self.feedItems.iter_next(it)
1122 #for index in self.feed.getIds():
1123 # self.feed.setEntryRead(index)
1124 # self.mark_item_read(index)
1130 self.window = hildon.StackableWindow()
1131 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1133 self.config = Config(self.window, CONFIGDIR+"config.ini")
1136 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1137 self.orientation.set_mode(self.config.getOrientation())
1138 except Exception, e:
1139 logger.warn("Could not start rotation manager: %s" % str(e))
1141 self.window.set_title(__appname__)
1142 self.mainVbox = gtk.VBox(False,10)
1144 if isfile(CONFIGDIR+"/feeds.db"):
1145 self.introLabel = gtk.Label("Loading...")
1147 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1149 self.mainVbox.pack_start(self.introLabel)
1151 self.window.add(self.mainVbox)
1152 self.window.show_all()
1153 gobject.idle_add(self.createWindow)
1155 def createWindow(self):
1157 self.listing = Listing(self.config, CONFIGDIR)
1159 self.downloadDialog = False
1161 self.introLabel.destroy()
1162 self.pannableListing = hildon.PannableArea()
1164 # The main area is a view consisting of an icon and two
1165 # strings. The view is bound to the Listing's database via a
1168 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1169 self.feedList = gtk.TreeView(self.feedItems)
1170 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1172 self.pannableListing.add(self.feedList)
1174 icon_renderer = gtk.CellRendererPixbuf()
1175 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1176 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1178 self.feedList.append_column(icon_column)
1180 markup_renderer = gtk.CellRendererText()
1181 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1182 markup=COLUMN_MARKUP)
1183 self.feedList.append_column(markup_column)
1184 self.mainVbox.pack_start(self.pannableListing)
1185 self.mainVbox.show_all()
1187 self.displayListing()
1189 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1190 gobject.idle_add(self.late_init)
1192 def update_progress(self, percent_complete,
1193 completed, in_progress, queued,
1194 bytes_downloaded, bytes_updated, bytes_per_second,
1196 if (in_progress or queued) and not self.downloadDialog:
1197 self.downloadDialog = DownloadBar(self.window)
1198 self.downloadDialog.connect("download-done", self.onDownloadDone)
1199 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1200 self.downloadDialog.show()
1202 if self.__dict__.get ('disp', None):
1203 self.disp.show_download_bar ()
1205 def onDownloadDone(self, widget, feed):
1207 self.downloadDialog.destroy()
1208 self.downloadDialog = False
1209 self.displayListing()
1211 def late_init(self):
1212 # Finish building the GUI.
1213 menu = hildon.AppMenu()
1214 # Create a button and add it to the menu
1215 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1216 button.set_label("Update feeds")
1217 button.connect("clicked", self.button_update_clicked, "All")
1220 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1221 button.set_label("Mark all as read")
1222 button.connect("clicked", self.button_markAll)
1225 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1226 button.set_label("Add new feed")
1227 button.connect("clicked", lambda b: self.addFeed())
1230 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1231 button.set_label("Manage subscriptions")
1232 button.connect("clicked", self.button_organize_clicked)
1235 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1236 button.set_label("Settings")
1237 button.connect("clicked", self.button_preferences_clicked)
1240 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1241 button.set_label("About")
1242 button.connect("clicked", self.button_about_clicked)
1245 self.window.set_app_menu(menu)
1248 # Initialize the DBus interface.
1249 self.dbusHandler = ServerObject(self)
1250 bus = dbus.SessionBus()
1251 bus.add_signal_receiver(handler_function=self.update_progress,
1253 signal_name='UpdateProgress',
1254 dbus_interface='org.marcoz.feedingit',
1255 path='/org/marcoz/feedingit/update')
1257 # Check whether auto-update is enabled.
1258 self.autoupdate = False
1259 self.checkAutoUpdate()
1261 gobject.idle_add(self.build_feed_display)
1263 def build_feed_display(self):
1264 if not hasattr(self, 'disp'):
1265 self.disp = DisplayFeed(self.listing, self.config)
1266 self.disp.connect("feed-closed", self.onFeedClosed)
1268 def button_markAll(self, button):
1269 for key in self.listing.getListOfFeeds():
1270 feed = self.listing.getFeed(key)
1271 feed.markAllAsRead()
1272 #for id in feed.getIds():
1273 # feed.setEntryRead(id)
1274 self.listing.updateUnread(key)
1275 self.displayListing()
1277 def button_about_clicked(self, button):
1278 HeAboutDialog.present(self.window, \
1288 def button_export_clicked(self, button):
1289 opml = ExportOpmlData(self.window, self.listing)
1291 def button_import_clicked(self, button):
1292 opml = GetOpmlData(self.window)
1293 feeds = opml.getData()
1294 for (title, url) in feeds:
1295 self.listing.addFeed(title, url)
1296 self.displayListing()
1298 def addFeed(self, urlIn="http://"):
1299 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1302 (title, url, category) = wizard.getData()
1304 self.listing.addFeed(title, url, category=category)
1306 self.displayListing()
1308 def button_organize_clicked(self, button):
1309 def after_closing():
1310 self.displayListing()
1311 SortList(self.window, self.listing, self, after_closing)
1313 def do_update_feeds(self):
1314 for k in self.listing.getListOfFeeds():
1315 self.listing.updateFeed (k)
1317 def button_update_clicked(self, button, key):
1318 gobject.idle_add(self.do_update_feeds)
1320 def onDownloadsDone(self, *widget):
1321 self.downloadDialog.destroy()
1322 self.downloadDialog = False
1323 self.displayListing()
1325 def button_preferences_clicked(self, button):
1326 dialog = self.config.createDialog()
1327 dialog.connect("destroy", self.prefsClosed)
1329 def show_confirmation_note(self, parent, title):
1330 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1332 retcode = gtk.Dialog.run(note)
1335 if retcode == gtk.RESPONSE_OK:
1340 def saveExpandedLines(self):
1341 self.expandedLines = []
1342 model = self.feedList.get_model()
1343 model.foreach(self.checkLine)
1345 def checkLine(self, model, path, iter, data = None):
1346 if self.feedList.row_expanded(path):
1347 self.expandedLines.append(path)
1349 def restoreExpandedLines(self):
1350 model = self.feedList.get_model()
1351 model.foreach(self.restoreLine)
1353 def restoreLine(self, model, path, iter, data = None):
1354 if path in self.expandedLines:
1355 self.feedList.expand_row(path, False)
1357 def displayListing(self):
1358 icon_theme = gtk.icon_theme_get_default()
1359 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1360 gtk.ICON_LOOKUP_USE_BUILTIN)
1362 self.saveExpandedLines()
1364 self.feedItems.clear()
1365 hideReadFeed = self.config.getHideReadFeeds()
1366 order = self.config.getFeedSortOrder()
1368 categories = self.listing.getListOfCategories()
1369 if len(categories) > 1:
1370 showCategories = True
1372 showCategories = False
1374 for categoryId in categories:
1376 title = self.listing.getCategoryTitle(categoryId)
1377 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1379 if showCategories and len(keys)>0:
1380 category = self.feedItems.append(None, (None, title, categoryId))
1381 #print "catID" + str(categoryId) + " " + str(self.category)
1382 if categoryId == self.category:
1384 expandedRow = category
1387 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1388 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1389 updateTime = self.listing.getFeedUpdateTime(key)
1390 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1392 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1394 markup = FEED_TEMPLATE % (title, subtitle)
1397 icon_filename = self.listing.getFavicon(key)
1398 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1399 LIST_ICON_SIZE, LIST_ICON_SIZE)
1401 pixbuf = default_pixbuf
1404 self.feedItems.append(category, (pixbuf, markup, key))
1406 self.feedItems.append(None, (pixbuf, markup, key))
1409 self.restoreExpandedLines()
1412 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1416 def on_feedList_row_activated(self, treeview, path, column):
1417 model = treeview.get_model()
1418 iter = model.get_iter(path)
1419 key = model.get_value(iter, COLUMN_KEY)
1422 #print "Key: " + str(key)
1424 self.category = catId
1425 if treeview.row_expanded(path):
1426 treeview.collapse_row(path)
1428 # treeview.expand_row(path, True)
1429 #treeview.collapse_all()
1430 #treeview.expand_row(path, False)
1431 #for i in range(len(path)):
1432 # self.feedList.expand_row(path[:i+1], False)
1433 #self.show_confirmation_note(self.window, "Working")
1439 def openFeed(self, key):
1441 self.build_feed_display()
1442 self.disp.displayFeed(key)
1444 def openArticle(self, key, id):
1447 self.disp.button_clicked(None, id)
1449 def onFeedClosed(self, object, key):
1450 gobject.idle_add(self.displayListing)
1452 def quit(self, *args):
1457 self.window.connect("destroy", self.quit)
1460 def prefsClosed(self, *widget):
1462 self.orientation.set_mode(self.config.getOrientation())
1465 self.displayListing()
1466 self.checkAutoUpdate()
1468 def checkAutoUpdate(self, *widget):
1469 interval = int(self.config.getUpdateInterval()*3600000)
1470 if self.config.isAutoUpdateEnabled():
1471 if self.autoupdate == False:
1472 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1473 self.autoupdate = interval
1474 elif not self.autoupdate == interval:
1475 # If auto-update is enabled, but not at the right frequency
1476 gobject.source_remove(self.autoupdateId)
1477 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1478 self.autoupdate = interval
1480 if not self.autoupdate == False:
1481 gobject.source_remove(self.autoupdateId)
1482 self.autoupdate = False
1484 def automaticUpdate(self, *widget):
1485 # Need to check for internet connection
1486 # If no internet connection, try again in 10 minutes:
1487 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1488 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1489 #from time import localtime, strftime
1490 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1492 self.button_update_clicked(None, None)
1495 def getStatus(self):
1497 for key in self.listing.getListOfFeeds():
1498 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1499 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1501 status = "No unread items"
1504 def grabFocus(self):
1505 self.window.present()
1507 if __name__ == "__main__":
1509 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1511 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1512 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1513 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1514 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1515 gobject.threads_init()
1516 if not isdir(CONFIGDIR):
1520 logger.error("Error: Can't create configuration directory")
1521 from sys import exit