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 # ============================================================================
28 from pango import FontDescription
33 from webkit import WebView
38 from os.path import isfile, isdir, exists
39 from os import mkdir, remove, stat, environ
41 from aboutdialog import HeAboutDialog
42 from portrait import FremantleRotation
43 from threading import Thread, activeCount
44 from feedingitdbus import ServerObject
45 from config import Config
46 from cgi import escape
51 logger = logging.getLogger(__name__)
53 from rss_sqlite import Listing
54 from opml import GetOpmlData, ExportOpmlData
58 from socket import setdefaulttimeout
60 setdefaulttimeout(timeout)
68 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
69 ABOUT_ICON = 'feedingit'
70 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
71 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
72 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
73 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
75 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
76 unread_color = color_style.lookup_color('ActiveTextColor')
77 read_color = color_style.lookup_color('DefaultTextColor')
80 CONFIGDIR="/home/user/.feedingit/"
81 LOCK = CONFIGDIR + "update.lock"
84 from htmlentitydefs import name2codepoint
86 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
88 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
92 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
93 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
94 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
96 # Build the markup template for the Maemo 5 text style
97 head_font = style.get_font_desc('SystemFont')
98 sub_font = style.get_font_desc('SmallSystemFont')
100 #head_color = style.get_color('ButtonTextColor')
101 head_color = style.get_color('DefaultTextColor')
102 sub_color = style.get_color('DefaultTextColor')
103 active_color = style.get_color('ActiveTextColor')
105 bg_color = style.get_color('DefaultBackgroundColor').to_string()
106 c1=hex(min(int(bg_color[1:5],16)+10000, 65535))[2:6]
107 c2=hex(min(int(bg_color[5:9],16)+10000, 65535))[2:6]
108 c3=hex(min(int(bg_color[9:],16)+10000, 65535))[2:6]
109 bg_color = "#" + c1 + c2 + c3
112 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
113 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
115 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
116 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
118 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
119 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
121 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
122 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
124 FEED_TEMPLATE = '\n'.join((head, normal_sub))
125 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
127 ENTRY_TEMPLATE = entry_head
128 ENTRY_TEMPLATE_UNREAD = entry_active_head
130 notification_iface = None
133 global notification_iface
135 bus = dbus.SessionBus()
136 proxy = bus.get_object('org.freedesktop.Notifications',
137 '/org/freedesktop/Notifications')
139 = dbus.Interface(proxy, 'org.freedesktop.Notifications')
142 notification_iface.SystemNoteInfoprint("FeedingIt: " + message)
144 if notification_iface is None:
149 except dbus.DBusException:
150 # Rebind the name and try again.
155 # Removes HTML or XML character references and entities from a text string.
157 # @param text The HTML (or XML) source text.
158 # @return The plain text, as a Unicode string, if necessary.
159 # http://effbot.org/zone/re-sub.htm#unescape-html
164 # character reference
166 if text[:3] == "&#x":
167 return unichr(int(text[3:-1], 16))
169 return unichr(int(text[2:-1]))
175 text = unichr(name2codepoint[text[1:-1]])
178 return text # leave as is
179 return sub("&#?\w+;", fixup, text)
182 class AddWidgetWizard(gtk.Dialog):
183 def __init__(self, parent, listing, urlIn, categories, titleIn=None, isEdit=False, currentCat=1):
184 gtk.Dialog.__init__(self)
185 self.set_transient_for(parent)
187 #self.category = categories[0]
188 self.category = currentCat
191 self.set_title('Edit RSS feed')
193 self.set_title('Add new RSS feed')
196 self.btn_add = self.add_button('Save', 2)
198 self.btn_add = self.add_button('Add', 2)
200 self.set_default_response(2)
202 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
203 self.nameEntry.set_placeholder('Feed name')
204 # If titleIn matches urlIn, there is no title.
205 if not titleIn == None and titleIn != urlIn:
206 self.nameEntry.set_text(titleIn)
207 self.nameEntry.select_region(-1, -1)
209 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
210 self.urlEntry.set_placeholder('Feed URL')
211 self.urlEntry.set_text(urlIn)
212 self.urlEntry.select_region(-1, -1)
213 self.urlEntry.set_activates_default(True)
215 self.table = gtk.Table(3, 2, False)
216 self.table.set_col_spacings(5)
217 label = gtk.Label('Name:')
218 label.set_alignment(1., .5)
219 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
220 self.table.attach(self.nameEntry, 1, 2, 0, 1)
221 label = gtk.Label('URL:')
222 label.set_alignment(1., .5)
223 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
224 self.table.attach(self.urlEntry, 1, 2, 1, 2)
225 selector = self.create_selector(categories, listing)
226 picker = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
227 picker.set_selector(selector)
228 picker.set_title("Select category")
229 #picker.set_text(listing.getCategoryTitle(self.category), None) #, "Subtitle")
230 picker.set_name('HildonButton-finger')
231 picker.set_alignment(0,0,1,1)
233 self.table.attach(picker, 0, 2, 2, 3, gtk.FILL)
235 self.vbox.pack_start(self.table)
240 return (self.nameEntry.get_text(), self.urlEntry.get_text(), self.category)
242 def create_selector(self, choices, listing):
243 #self.pickerDialog = hildon.PickerDialog(self.parent)
244 selector = hildon.TouchSelector(text=True)
248 title = listing.getCategoryTitle(item)
249 iter = selector.append_text(str(title))
250 if self.category == item:
251 selector.set_active(0, index)
252 self.map[title] = item
254 selector.connect("changed", self.selection_changed)
255 #self.pickerDialog.set_selector(selector)
258 def selection_changed(self, selector, button):
259 current_selection = selector.get_current_text()
260 if current_selection:
261 self.category = self.map[current_selection]
263 class AddCategoryWizard(gtk.Dialog):
264 def __init__(self, parent, titleIn=None, isEdit=False):
265 gtk.Dialog.__init__(self)
266 self.set_transient_for(parent)
269 self.set_title('Edit Category')
271 self.set_title('Add Category')
274 self.btn_add = self.add_button('Save', 2)
276 self.btn_add = self.add_button('Add', 2)
278 self.set_default_response(2)
280 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
281 self.nameEntry.set_placeholder('Category name')
282 if not titleIn == None:
283 self.nameEntry.set_text(titleIn)
284 self.nameEntry.select_region(-1, -1)
286 self.table = gtk.Table(1, 2, False)
287 self.table.set_col_spacings(5)
288 label = gtk.Label('Name:')
289 label.set_alignment(1., .5)
290 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
291 self.table.attach(self.nameEntry, 1, 2, 0, 1)
292 #label = gtk.Label('URL:')
293 #label.set_alignment(1., .5)
294 #self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
295 #self.table.attach(self.urlEntry, 1, 2, 1, 2)
296 self.vbox.pack_start(self.table)
301 return self.nameEntry.get_text()
303 class DownloadBar(gtk.ProgressBar):
306 if hasattr (cls, 'class_init_done'):
309 cls.downloadbars = []
310 # Total number of jobs we are monitoring.
312 # Number of jobs complete (of those that we are monitoring).
317 cls.class_init_done = True
319 bus = dbus.SessionBus()
320 bus.add_signal_receiver(handler_function=cls.update_progress,
322 signal_name='UpdateProgress',
323 dbus_interface='org.marcoz.feedingit',
324 path='/org/marcoz/feedingit/update')
326 def __init__(self, parent):
329 gtk.ProgressBar.__init__(self)
331 self.downloadbars.append(weakref.ref (self))
333 self.__class__.update_bars()
336 def downloading(cls):
338 return cls.done != cls.total
341 def update_progress(cls, percent_complete,
342 completed, in_progress, queued,
343 bytes_downloaded, bytes_updated, bytes_per_second,
345 if not cls.downloadbars:
348 cls.total = completed + in_progress + queued
350 cls.progress = percent_complete / 100.
351 if cls.progress < 0: cls.progress = 0
352 if cls.progress > 1: cls.progress = 1
355 for ref in cls.downloadbars:
358 # The download bar disappeared.
359 cls.downloadbars.remove (ref)
361 bar.emit("download-done", feed_updated)
363 if in_progress == 0 and queued == 0:
364 for ref in cls.downloadbars:
367 # The download bar disappeared.
368 cls.downloadbars.remove (ref)
370 bar.emit("download-done", None)
376 def update_bars(cls):
377 # In preparation for i18n/l10n
379 return (a if n == 1 else b)
381 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
383 % (cls.done, cls.total))
385 for ref in cls.downloadbars:
388 # The download bar disappeared.
389 cls.downloadbars.remove (ref)
392 bar.set_fraction(cls.progress)
394 class SortList(hildon.StackableWindow):
395 def __init__(self, parent, listing, feedingit, after_closing, category=None):
396 hildon.StackableWindow.__init__(self)
397 self.set_transient_for(parent)
399 self.isEditingCategories = False
400 self.category = category
401 self.set_title(listing.getCategoryTitle(category))
403 self.isEditingCategories = True
404 self.set_title('Categories')
405 self.listing = listing
406 self.feedingit = feedingit
407 self.after_closing = after_closing
409 self.connect('destroy', lambda w: self.after_closing())
410 self.vbox2 = gtk.VBox(False, 2)
412 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
413 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
414 button.connect("clicked", self.buttonUp)
415 self.vbox2.pack_start(button, expand=False, fill=False)
417 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
418 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
419 button.connect("clicked", self.buttonDown)
420 self.vbox2.pack_start(button, expand=False, fill=False)
422 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
424 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
425 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
426 button.connect("clicked", self.buttonAdd)
427 self.vbox2.pack_start(button, expand=False, fill=False)
429 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
430 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
431 button.connect("clicked", self.buttonEdit)
432 self.vbox2.pack_start(button, expand=False, fill=False)
434 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
435 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
436 button.connect("clicked", self.buttonDelete)
437 self.vbox2.pack_start(button, expand=False, fill=False)
439 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
440 #button.set_label("Done")
441 #button.connect("clicked", self.buttonDone)
442 #self.vbox.pack_start(button)
443 self.hbox2= gtk.HBox(False, 10)
444 self.pannableArea = hildon.PannableArea()
445 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
446 self.treeview = gtk.TreeView(self.treestore)
447 self.hbox2.pack_start(self.pannableArea, expand=True)
449 self.hbox2.pack_end(self.vbox2, expand=False)
450 self.set_default_size(-1, 600)
453 menu = hildon.AppMenu()
454 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
455 button.set_label("Import from OPML")
456 button.connect("clicked", self.feedingit.button_import_clicked)
459 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
460 button.set_label("Export to OPML")
461 button.connect("clicked", self.feedingit.button_export_clicked)
463 self.set_app_menu(menu)
467 #self.connect("destroy", self.buttonDone)
469 def displayFeeds(self):
470 self.treeview.destroy()
471 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
472 self.treeview = gtk.TreeView()
474 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
475 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
477 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
479 self.pannableArea.add(self.treeview)
483 def refreshList(self, selected=None, offset=0):
484 #rect = self.treeview.get_visible_rect()
485 #y = rect.y+rect.height
486 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
487 if self.isEditingCategories:
488 for key in self.listing.getListOfCategories():
489 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
493 for key in self.listing.getListOfFeeds(category=self.category):
494 item = self.treestore.append([self.listing.getFeedTitle(key), key])
497 self.treeview.set_model(self.treestore)
498 if not selected == None:
499 self.treeview.get_selection().select_iter(selectedItem)
500 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
501 self.pannableArea.show_all()
503 def getSelectedItem(self):
504 (model, iter) = self.treeview.get_selection().get_selected()
507 return model.get_value(iter, 1)
509 def findIndex(self, key):
513 for row in self.treestore:
515 return (before, row.iter)
516 if key == list(row)[0]:
520 return (before, None)
522 def buttonUp(self, button):
523 key = self.getSelectedItem()
525 if self.isEditingCategories:
526 self.listing.moveCategoryUp(key)
528 self.listing.moveUp(key)
529 self.refreshList(key, -10)
531 def buttonDown(self, button):
532 key = self.getSelectedItem()
534 if self.isEditingCategories:
535 self.listing.moveCategoryDown(key)
537 self.listing.moveDown(key)
538 self.refreshList(key, 10)
540 def buttonDelete(self, button):
541 key = self.getSelectedItem()
543 message = 'Really remove this feed and its entries?'
544 dlg = hildon.hildon_note_new_confirmation(self, message)
547 if response == gtk.RESPONSE_OK:
548 if self.isEditingCategories:
549 self.listing.removeCategory(key)
551 self.listing.removeFeed(key)
554 def buttonEdit(self, button):
555 key = self.getSelectedItem()
557 if key == 'ArchivedArticles':
558 message = 'Cannot edit the archived articles feed.'
559 hildon.hildon_banner_show_information(self, '', message)
561 if self.isEditingCategories:
563 SortList(self.parent, self.listing, self.feedingit, None, category=key)
566 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
569 (title, url, category) = wizard.getData()
571 self.listing.editFeed(key, title, url, category=category)
575 def buttonDone(self, *args):
578 def buttonAdd(self, button, urlIn="http://"):
579 if self.isEditingCategories:
580 wizard = AddCategoryWizard(self)
583 title = wizard.getData()
584 if (not title == ''):
585 self.listing.addCategory(title)
587 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
590 (title, url, category) = wizard.getData()
592 self.listing.addFeed(title, url, category=category)
597 class DisplayArticle(hildon.StackableWindow):
599 A Widget for displaying an article.
601 def __init__(self, article_id, feed, feed_key, articles, config, listing):
603 article_id - The identifier of the article to load.
605 feed - The feed object containing the article (an
606 rss_sqlite:Feed object).
608 feed_key - The feed's identifier.
610 articles - A list of articles from the feed to display.
611 Needed for selecting the next/previous article (article_next).
613 config - A configuration object (config:Config).
615 listing - The listing object (rss_sqlite:Listing) that
616 contains the feed and article.
618 hildon.StackableWindow.__init__(self)
620 self.article_id = None
622 self.feed_key = feed_key
623 self.articles = articles
625 self.listing = listing
627 self.set_title(self.listing.getFeedTitle(feed_key))
629 # Init the article display
630 self.view = WebView()
631 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
632 self.view.connect("motion-notify-event", lambda w,ev: True)
633 self.view.connect('load-started', self.load_started)
634 self.view.connect('load-finished', self.load_finished)
635 self.view.connect('navigation-requested', self.navigation_requested)
636 self.view.connect("button_press_event", self.button_pressed)
637 self.gestureId = self.view.connect(
638 "button_release_event", self.button_released)
640 self.pannable_article = hildon.PannableArea()
641 self.pannable_article.add(self.view)
643 self.add(self.pannable_article)
645 self.pannable_article.show_all()
648 menu = hildon.AppMenu()
650 def menu_button(label, callback):
651 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
652 button.set_label(label)
653 button.connect("clicked", callback)
656 menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
657 menu_button("Open in browser", self.open_in_browser)
658 if feed_key == "ArchivedArticles":
660 "Remove from archived articles", self.remove_archive_button)
662 menu_button("Add to archived articles", self.archive_button)
664 self.set_app_menu(menu)
667 self.destroyId = self.connect("destroy", self.destroyWindow)
669 self.article_open(article_id)
671 def article_open(self, article_id):
673 Load the article with the specified id.
675 # If an article was open, close it.
676 if self.article_id is not None:
677 self.article_closed()
679 self.article_id = article_id
680 self.set_for_removal = False
681 self.loadedArticle = False
682 self.initial_article_load = True
684 contentLink = self.feed.getContentLink(self.article_id)
685 if contentLink.startswith("/home/user/"):
686 self.view.open("file://%s" % contentLink)
687 self.currentUrl = self.feed.getExternalLink(self.article_id)
689 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
690 self.currentUrl = str(contentLink)
692 self.feed.setEntryRead(self.article_id)
694 def article_closed(self):
696 The user has navigated away from the article. Execute any
699 if self.set_for_removal:
700 self.emit("article-deleted", self.article_id)
702 self.emit("article-closed", self.article_id)
705 def navigation_requested(self, wv, fr, req):
707 http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
710 fr - a WebKitWebFrame
711 req - WebKitNetworkRequest
713 if self.initial_article_load:
714 # Always initially load an article in the internal
716 self.initial_article_load = False
719 # When following a link, only use the internal browser if so
720 # configured. Otherwise, launch an external browser.
721 if self.config.getOpenInExternalBrowser():
722 self.open_in_browser(None, req.get_uri())
727 def load_started(self, *widget):
728 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
730 def load_finished(self, *widget):
731 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
732 frame = self.view.get_main_frame()
733 if self.loadedArticle:
734 self.currentUrl = frame.get_uri()
736 self.loadedArticle = True
738 def button_pressed(self, window, event):
740 The user pressed a "mouse button" (in our case, this means the
741 user likely started to drag with the finger).
743 We are only interested in whether the user performs a drag.
744 We record the starting position and when the user "releases
745 the button," we see how far the mouse moved.
747 self.coords = (event.x, event.y)
749 def button_released(self, window, event):
750 x = self.coords[0] - event.x
751 y = self.coords[1] - event.y
753 if (2*abs(y) < abs(x)):
755 self.article_next(forward=False)
757 self.article_next(forward=True)
759 # We handled the event. Don't propagate it further.
762 def article_next(self, forward=True):
764 Advance to the next (or, if forward is false, the previous)
772 id = self.feed.getNextId(id, forward)
780 if id in self.articles:
781 self.article_open(id)
784 def destroyWindow(self, *args):
785 self.article_closed()
786 self.disconnect(self.destroyId)
789 def horiz_scrolling_button(self, *widget):
790 self.pannable_article.disconnect(self.gestureId)
791 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
793 def archive_button(self, *widget):
794 # Call the listing.addArchivedArticle
795 self.listing.addArchivedArticle(self.feed_key, self.article_id)
797 def remove_archive_button(self, *widget):
798 self.set_for_removal = True
800 def open_in_browser(self, object, link=None):
802 Open the specified link using the system's browser. If not
803 link is specified, reopen the current page using the system's
807 link = self.currentUrl
809 bus = dbus.SessionBus()
810 b_proxy = bus.get_object("com.nokia.osso_browser",
811 "/com/nokia/osso_browser/request")
812 b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
814 notify("Opening %s" % link)
816 # We open the link asynchronously: if the web browser is not
817 # already running, this can take a while.
820 Something went wrong opening the URL.
823 notify("Error opening %s: %s" % (link, str(exception)))
826 b_iface.open_new_window(link,
827 reply_handler=lambda *args: None,
828 error_handler=error_handler())
830 class DisplayFeed(hildon.StackableWindow):
831 def __init__(self, listing, config):
832 hildon.StackableWindow.__init__(self)
833 self.connect('configure-event', self.on_configure_event)
834 self.connect("delete_event", self.delete_window)
835 self.connect("destroy", self.destroyWindow)
837 self.listing = listing
842 # If hide read articles is set, this is set to the set of
843 # unread articles at the time that feed is loaded. The last
844 # bit is important: when the user selects the next article,
845 # but then decides to move back, previous should select the
847 self.articles = list()
849 self.downloadDialog = False
851 #self.listing.setCurrentlyDisplayedFeed(self.key)
855 menu = hildon.AppMenu()
856 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
857 button.set_label("Update feed")
858 button.connect("clicked", self.button_update_clicked)
861 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
862 button.set_label("Mark all as read")
863 button.connect("clicked", self.buttonReadAllClicked)
866 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
867 button.set_label("Delete read articles")
868 button.connect("clicked", self.buttonPurgeArticles)
870 self.archived_article_buttons = [button]
872 self.set_app_menu(menu)
875 self.feedItems = gtk.ListStore(str, str)
877 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
878 self.feedList.set_model(self.feedItems)
879 self.feedList.set_rules_hint(True)
880 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
881 self.feedList.set_hover_selection(False)
882 self.feedList.connect('hildon-row-tapped',
883 self.on_feedList_row_activated)
885 self.markup_renderer = gtk.CellRendererText()
886 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
887 self.markup_renderer.set_property('background', bg_color) #"#333333")
888 (width, height) = self.get_size()
889 self.markup_renderer.set_property('wrap-width', width-20)
890 self.markup_renderer.set_property('ypad', 8)
891 self.markup_renderer.set_property('xpad', 5)
892 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
893 markup=FEED_COLUMN_MARKUP)
894 self.feedList.append_column(markup_column)
896 vbox = gtk.VBox(False, 10)
897 vbox.pack_start(self.feedList)
899 self.pannableFeed = hildon.PannableArea()
900 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
901 self.pannableFeed.add_with_viewport(vbox)
903 self.main_vbox = gtk.VBox(False, 0)
904 self.main_vbox.pack_start(self.pannableFeed)
906 self.add(self.main_vbox)
908 self.main_vbox.show_all()
910 def on_configure_event(self, window, event):
911 if getattr(self, 'markup_renderer', None) is None:
914 # Fix up the column width for wrapping the text when the window is
915 # resized (i.e. orientation changed)
916 self.markup_renderer.set_property('wrap-width', event.width-20)
917 it = self.feedItems.get_iter_first()
918 while it is not None:
919 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
920 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
921 it = self.feedItems.iter_next(it)
923 def delete_window(self, *args):
925 Prevent the window from being deleted.
931 except AttributeError:
935 self.listing.updateUnread(key)
936 self.emit("feed-closed", key)
938 self.feedItems.clear()
942 def destroyWindow(self, *args):
945 except AttributeError:
950 def fix_title(self, title):
951 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
953 def displayFeed(self, key=None):
955 Select and display a feed. If feed, title and key are None,
956 reloads the current feed.
961 except AttributeError:
965 self.listing.updateUnread(self.key)
966 self.emit("feed-closed", self.key)
968 self.feed = self.listing.getFeed(key)
969 self.feedTitle = self.listing.getFeedTitle(key)
972 self.set_title(self.feedTitle)
974 selection = self.feedList.get_selection()
975 if selection is not None:
976 selection.set_mode(gtk.SELECTION_NONE)
978 for b in self.archived_article_buttons:
979 if key == "ArchivedArticles":
984 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
985 hideReadArticles = self.config.getHideReadArticles()
987 articles = self.feed.getIds(onlyUnread=True)
989 articles = self.feed.getIds()
991 self.articles[:] = []
993 self.feedItems.clear()
996 isRead = self.feed.isEntryRead(id)
999 if not ( isRead and hideReadArticles ):
1000 title = self.fix_title(self.feed.getTitle(id))
1001 self.articles.append(id)
1003 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1005 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
1007 self.feedItems.append((markup, id))
1010 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
1011 self.feedItems.append((markup, ""))
1016 self.pannableFeed.destroy()
1017 #self.remove(self.pannableFeed)
1019 def on_feedList_row_activated(self, treeview, path): #, column):
1020 if not self.articles:
1021 # There are not actually any articles. Ignore.
1024 selection = self.feedList.get_selection()
1025 selection.set_mode(gtk.SELECTION_SINGLE)
1026 self.feedList.get_selection().select_path(path)
1027 model = treeview.get_model()
1028 iter = model.get_iter(path)
1029 key = model.get_value(iter, FEED_COLUMN_KEY)
1030 # Emulate legacy "button_clicked" call via treeview
1031 gobject.idle_add(self.button_clicked, treeview, key)
1034 def button_clicked(self, button, index, previous=False, next=False):
1035 newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
1036 stack = hildon.WindowStack.get_default()
1039 stack.pop_and_push(1, newDisp, tmp)
1041 gobject.timeout_add(200, self.destroyArticle, tmp)
1046 if type(self.disp).__name__ == "DisplayArticle":
1047 gobject.timeout_add(200, self.destroyArticle, self.disp)
1051 self.disp.show_all()
1054 if self.key == "ArchivedArticles":
1055 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
1056 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
1058 def buttonPurgeArticles(self, *widget):
1060 self.feed.purgeReadArticles()
1061 #self.feed.saveFeed(CONFIGDIR)
1064 def destroyArticle(self, handle):
1065 handle.destroyWindow()
1067 def mark_item_read(self, key):
1068 it = self.feedItems.get_iter_first()
1069 while it is not None:
1070 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1072 title = self.fix_title(self.feed.getTitle(key))
1073 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1074 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1076 it = self.feedItems.iter_next(it)
1078 def onArticleClosed(self, object, index):
1079 selection = self.feedList.get_selection()
1080 selection.set_mode(gtk.SELECTION_NONE)
1081 self.mark_item_read(index)
1083 def onArticleDeleted(self, object, index):
1085 self.feed.removeArticle(index)
1086 #self.feed.saveFeed(CONFIGDIR)
1090 def do_update_feed(self):
1091 self.listing.updateFeed (self.key, priority=-1)
1093 def button_update_clicked(self, button):
1094 gobject.idle_add(self.do_update_feed)
1096 def show_download_bar(self):
1097 if not type(self.downloadDialog).__name__=="DownloadBar":
1098 self.downloadDialog = DownloadBar(self.window)
1099 self.downloadDialog.connect("download-done", self.onDownloadDone)
1100 self.main_vbox.pack_end(self.downloadDialog,
1101 expand=False, fill=False)
1102 self.downloadDialog.show()
1104 def onDownloadDone(self, widget, feed):
1105 if feed is not None and hasattr(self, 'feed') and feed == self.feed:
1106 self.feed = self.listing.getFeed(self.key)
1110 self.downloadDialog.destroy()
1111 self.downloadDialog = False
1113 def buttonReadAllClicked(self, button):
1115 self.feed.markAllAsRead()
1116 it = self.feedItems.get_iter_first()
1117 while it is not None:
1118 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1119 title = self.fix_title(self.feed.getTitle(k))
1120 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1121 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1122 it = self.feedItems.iter_next(it)
1124 #for index in self.feed.getIds():
1125 # self.feed.setEntryRead(index)
1126 # self.mark_item_read(index)
1132 self.window = hildon.StackableWindow()
1133 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1135 self.config = Config(self.window, CONFIGDIR+"config.ini")
1138 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1139 self.orientation.set_mode(self.config.getOrientation())
1140 except Exception, e:
1141 logger.warn("Could not start rotation manager: %s" % str(e))
1143 self.window.set_title(__appname__)
1144 self.mainVbox = gtk.VBox(False,10)
1146 if isfile(CONFIGDIR+"/feeds.db"):
1147 self.introLabel = gtk.Label("Loading...")
1149 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1151 self.mainVbox.pack_start(self.introLabel)
1153 self.window.add(self.mainVbox)
1154 self.window.show_all()
1155 gobject.idle_add(self.createWindow)
1157 def createWindow(self):
1159 self.listing = Listing(self.config, CONFIGDIR)
1161 self.downloadDialog = False
1163 menu = hildon.AppMenu()
1164 # Create a button and add it to the menu
1165 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1166 button.set_label("Update feeds")
1167 button.connect("clicked", self.button_update_clicked, "All")
1170 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1171 button.set_label("Mark all as read")
1172 button.connect("clicked", self.button_markAll)
1175 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1176 button.set_label("Add new feed")
1177 button.connect("clicked", lambda b: self.addFeed())
1180 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1181 button.set_label("Manage subscriptions")
1182 button.connect("clicked", self.button_organize_clicked)
1185 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1186 button.set_label("Settings")
1187 button.connect("clicked", self.button_preferences_clicked)
1190 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1191 button.set_label("About")
1192 button.connect("clicked", self.button_about_clicked)
1195 self.window.set_app_menu(menu)
1198 #self.feedWindow = hildon.StackableWindow()
1199 #self.articleWindow = hildon.StackableWindow()
1200 self.introLabel.destroy()
1201 self.pannableListing = hildon.PannableArea()
1202 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1203 self.feedList = gtk.TreeView(self.feedItems)
1204 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1205 #self.feedList.set_enable_tree_lines(True)
1206 #self.feedList.set_show_expanders(True)
1207 self.pannableListing.add(self.feedList)
1209 icon_renderer = gtk.CellRendererPixbuf()
1210 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1211 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1213 self.feedList.append_column(icon_column)
1215 markup_renderer = gtk.CellRendererText()
1216 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1217 markup=COLUMN_MARKUP)
1218 self.feedList.append_column(markup_column)
1219 self.mainVbox.pack_start(self.pannableListing)
1220 self.mainVbox.show_all()
1222 self.displayListing()
1223 self.autoupdate = False
1224 self.checkAutoUpdate()
1226 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1227 gobject.idle_add(self.late_init)
1229 def update_progress(self, percent_complete,
1230 completed, in_progress, queued,
1231 bytes_downloaded, bytes_updated, bytes_per_second,
1233 if (in_progress or queued) and not self.downloadDialog:
1234 self.downloadDialog = DownloadBar(self.window)
1235 self.downloadDialog.connect("download-done", self.onDownloadDone)
1236 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1237 self.downloadDialog.show()
1239 if self.__dict__.get ('disp', None):
1240 self.disp.show_download_bar ()
1242 def onDownloadDone(self, widget, feed):
1244 self.downloadDialog.destroy()
1245 self.downloadDialog = False
1246 self.displayListing()
1248 def late_init(self):
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 gobject.idle_add(self.build_feed_display)
1259 def build_feed_display(self):
1260 if not hasattr(self, 'disp'):
1261 self.disp = DisplayFeed(self.listing, self.config)
1262 self.disp.connect("feed-closed", self.onFeedClosed)
1264 def button_markAll(self, button):
1265 for key in self.listing.getListOfFeeds():
1266 feed = self.listing.getFeed(key)
1267 feed.markAllAsRead()
1268 #for id in feed.getIds():
1269 # feed.setEntryRead(id)
1270 self.listing.updateUnread(key)
1271 self.displayListing()
1273 def button_about_clicked(self, button):
1274 HeAboutDialog.present(self.window, \
1284 def button_export_clicked(self, button):
1285 opml = ExportOpmlData(self.window, self.listing)
1287 def button_import_clicked(self, button):
1288 opml = GetOpmlData(self.window)
1289 feeds = opml.getData()
1290 for (title, url) in feeds:
1291 self.listing.addFeed(title, url)
1292 self.displayListing()
1294 def addFeed(self, urlIn="http://"):
1295 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1298 (title, url, category) = wizard.getData()
1300 self.listing.addFeed(title, url, category=category)
1302 self.displayListing()
1304 def button_organize_clicked(self, button):
1305 def after_closing():
1306 self.displayListing()
1307 SortList(self.window, self.listing, self, after_closing)
1309 def do_update_feeds(self):
1310 for k in self.listing.getListOfFeeds():
1311 self.listing.updateFeed (k)
1313 def button_update_clicked(self, button, key):
1314 gobject.idle_add(self.do_update_feeds)
1316 def onDownloadsDone(self, *widget):
1317 self.downloadDialog.destroy()
1318 self.downloadDialog = False
1319 self.displayListing()
1321 def button_preferences_clicked(self, button):
1322 dialog = self.config.createDialog()
1323 dialog.connect("destroy", self.prefsClosed)
1325 def show_confirmation_note(self, parent, title):
1326 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1328 retcode = gtk.Dialog.run(note)
1331 if retcode == gtk.RESPONSE_OK:
1336 def saveExpandedLines(self):
1337 self.expandedLines = []
1338 model = self.feedList.get_model()
1339 model.foreach(self.checkLine)
1341 def checkLine(self, model, path, iter, data = None):
1342 if self.feedList.row_expanded(path):
1343 self.expandedLines.append(path)
1345 def restoreExpandedLines(self):
1346 model = self.feedList.get_model()
1347 model.foreach(self.restoreLine)
1349 def restoreLine(self, model, path, iter, data = None):
1350 if path in self.expandedLines:
1351 self.feedList.expand_row(path, False)
1353 def displayListing(self):
1354 icon_theme = gtk.icon_theme_get_default()
1355 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1356 gtk.ICON_LOOKUP_USE_BUILTIN)
1358 self.saveExpandedLines()
1360 self.feedItems.clear()
1361 hideReadFeed = self.config.getHideReadFeeds()
1362 order = self.config.getFeedSortOrder()
1364 categories = self.listing.getListOfCategories()
1365 if len(categories) > 1:
1366 showCategories = True
1368 showCategories = False
1370 for categoryId in categories:
1372 title = self.listing.getCategoryTitle(categoryId)
1373 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1375 if showCategories and len(keys)>0:
1376 category = self.feedItems.append(None, (None, title, categoryId))
1377 #print "catID" + str(categoryId) + " " + str(self.category)
1378 if categoryId == self.category:
1380 expandedRow = category
1383 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1384 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1385 updateTime = self.listing.getFeedUpdateTime(key)
1386 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1388 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1390 markup = FEED_TEMPLATE % (title, subtitle)
1393 icon_filename = self.listing.getFavicon(key)
1394 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1395 LIST_ICON_SIZE, LIST_ICON_SIZE)
1397 pixbuf = default_pixbuf
1400 self.feedItems.append(category, (pixbuf, markup, key))
1402 self.feedItems.append(None, (pixbuf, markup, key))
1405 self.restoreExpandedLines()
1408 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1412 def on_feedList_row_activated(self, treeview, path, column):
1413 model = treeview.get_model()
1414 iter = model.get_iter(path)
1415 key = model.get_value(iter, COLUMN_KEY)
1418 #print "Key: " + str(key)
1420 self.category = catId
1421 if treeview.row_expanded(path):
1422 treeview.collapse_row(path)
1424 # treeview.expand_row(path, True)
1425 #treeview.collapse_all()
1426 #treeview.expand_row(path, False)
1427 #for i in range(len(path)):
1428 # self.feedList.expand_row(path[:i+1], False)
1429 #self.show_confirmation_note(self.window, "Working")
1435 def openFeed(self, key):
1437 self.build_feed_display()
1438 self.disp.displayFeed(key)
1440 def openArticle(self, key, id):
1443 self.disp.button_clicked(None, id)
1445 def onFeedClosed(self, object, key):
1446 gobject.idle_add(self.displayListing)
1448 def quit(self, *args):
1453 self.window.connect("destroy", self.quit)
1456 def prefsClosed(self, *widget):
1458 self.orientation.set_mode(self.config.getOrientation())
1461 self.displayListing()
1462 self.checkAutoUpdate()
1464 def checkAutoUpdate(self, *widget):
1465 interval = int(self.config.getUpdateInterval()*3600000)
1466 if self.config.isAutoUpdateEnabled():
1467 if self.autoupdate == False:
1468 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1469 self.autoupdate = interval
1470 elif not self.autoupdate == interval:
1471 # If auto-update is enabled, but not at the right frequency
1472 gobject.source_remove(self.autoupdateId)
1473 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1474 self.autoupdate = interval
1476 if not self.autoupdate == False:
1477 gobject.source_remove(self.autoupdateId)
1478 self.autoupdate = False
1480 def automaticUpdate(self, *widget):
1481 # Need to check for internet connection
1482 # If no internet connection, try again in 10 minutes:
1483 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1484 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1485 #from time import localtime, strftime
1486 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1488 self.button_update_clicked(None, None)
1491 def getStatus(self):
1493 for key in self.listing.getListOfFeeds():
1494 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1495 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1497 status = "No unread items"
1500 def grabFocus(self):
1501 self.window.present()
1503 if __name__ == "__main__":
1505 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1507 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1508 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1509 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1510 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1511 gobject.threads_init()
1512 if not isdir(CONFIGDIR):
1516 logger.error("Error: Can't create configuration directory")
1517 from sys import exit