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()
337 def downloading(cls):
339 return cls.done != cls.total
342 def update_progress(cls, percent_complete,
343 completed, in_progress, queued,
344 bytes_downloaded, bytes_updated, bytes_per_second,
346 if not cls.downloadbars:
349 cls.total = completed + in_progress + queued
351 cls.progress = percent_complete / 100.
352 if cls.progress < 0: cls.progress = 0
353 if cls.progress > 1: cls.progress = 1
356 for ref in cls.downloadbars:
359 # The download bar disappeared.
360 cls.downloadbars.remove (ref)
362 bar.emit("download-done", feed_updated)
364 if in_progress == 0 and queued == 0:
365 for ref in cls.downloadbars:
368 # The download bar disappeared.
369 cls.downloadbars.remove (ref)
371 bar.emit("download-done", None)
377 def update_bars(cls):
378 # In preparation for i18n/l10n
380 return (a if n == 1 else b)
382 text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
384 % (cls.done, cls.total))
386 for ref in cls.downloadbars:
389 # The download bar disappeared.
390 cls.downloadbars.remove (ref)
393 bar.set_fraction(cls.progress)
395 class SortList(hildon.StackableWindow):
396 def __init__(self, parent, listing, feedingit, after_closing, category=None):
397 hildon.StackableWindow.__init__(self)
398 self.set_transient_for(parent)
400 self.isEditingCategories = False
401 self.category = category
402 self.set_title(listing.getCategoryTitle(category))
404 self.isEditingCategories = True
405 self.set_title('Categories')
406 self.listing = listing
407 self.feedingit = feedingit
408 self.after_closing = after_closing
410 self.connect('destroy', lambda w: self.after_closing())
411 self.vbox2 = gtk.VBox(False, 2)
413 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
414 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
415 button.connect("clicked", self.buttonUp)
416 self.vbox2.pack_start(button, expand=False, fill=False)
418 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
419 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
420 button.connect("clicked", self.buttonDown)
421 self.vbox2.pack_start(button, expand=False, fill=False)
423 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
425 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
426 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
427 button.connect("clicked", self.buttonAdd)
428 self.vbox2.pack_start(button, expand=False, fill=False)
430 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
431 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
432 button.connect("clicked", self.buttonEdit)
433 self.vbox2.pack_start(button, expand=False, fill=False)
435 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
436 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
437 button.connect("clicked", self.buttonDelete)
438 self.vbox2.pack_start(button, expand=False, fill=False)
440 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
441 #button.set_label("Done")
442 #button.connect("clicked", self.buttonDone)
443 #self.vbox.pack_start(button)
444 self.hbox2= gtk.HBox(False, 10)
445 self.pannableArea = hildon.PannableArea()
446 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
447 self.treeview = gtk.TreeView(self.treestore)
448 self.hbox2.pack_start(self.pannableArea, expand=True)
450 self.hbox2.pack_end(self.vbox2, expand=False)
451 self.set_default_size(-1, 600)
454 menu = hildon.AppMenu()
455 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
456 button.set_label("Import from OPML")
457 button.connect("clicked", self.feedingit.button_import_clicked)
460 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
461 button.set_label("Export to OPML")
462 button.connect("clicked", self.feedingit.button_export_clicked)
464 self.set_app_menu(menu)
468 #self.connect("destroy", self.buttonDone)
470 def displayFeeds(self):
471 self.treeview.destroy()
472 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
473 self.treeview = gtk.TreeView()
475 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
476 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
478 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
480 self.pannableArea.add(self.treeview)
484 def refreshList(self, selected=None, offset=0):
485 #rect = self.treeview.get_visible_rect()
486 #y = rect.y+rect.height
487 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
488 if self.isEditingCategories:
489 for key in self.listing.getListOfCategories():
490 item = self.treestore.append([self.listing.getCategoryTitle(key), key])
494 for key in self.listing.getListOfFeeds(category=self.category):
495 item = self.treestore.append([self.listing.getFeedTitle(key), key])
498 self.treeview.set_model(self.treestore)
499 if not selected == None:
500 self.treeview.get_selection().select_iter(selectedItem)
501 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
502 self.pannableArea.show_all()
504 def getSelectedItem(self):
505 (model, iter) = self.treeview.get_selection().get_selected()
508 return model.get_value(iter, 1)
510 def findIndex(self, key):
514 for row in self.treestore:
516 return (before, row.iter)
517 if key == list(row)[0]:
521 return (before, None)
523 def buttonUp(self, button):
524 key = self.getSelectedItem()
526 if self.isEditingCategories:
527 self.listing.moveCategoryUp(key)
529 self.listing.moveUp(key)
530 self.refreshList(key, -10)
532 def buttonDown(self, button):
533 key = self.getSelectedItem()
535 if self.isEditingCategories:
536 self.listing.moveCategoryDown(key)
538 self.listing.moveDown(key)
539 self.refreshList(key, 10)
541 def buttonDelete(self, button):
542 key = self.getSelectedItem()
544 message = 'Really remove this feed and its entries?'
545 dlg = hildon.hildon_note_new_confirmation(self, message)
548 if response == gtk.RESPONSE_OK:
549 if self.isEditingCategories:
550 self.listing.removeCategory(key)
552 self.listing.removeFeed(key)
555 def buttonEdit(self, button):
556 key = self.getSelectedItem()
558 if key == 'ArchivedArticles':
559 message = 'Cannot edit the archived articles feed.'
560 hildon.hildon_banner_show_information(self, '', message)
562 if self.isEditingCategories:
564 SortList(self.parent, self.listing, self.feedingit, None, category=key)
567 wizard = AddWidgetWizard(self, self.listing, self.listing.getFeedUrl(key), self.listing.getListOfCategories(), self.listing.getFeedTitle(key), True, currentCat=self.category)
570 (title, url, category) = wizard.getData()
572 self.listing.editFeed(key, title, url, category=category)
576 def buttonDone(self, *args):
579 def buttonAdd(self, button, urlIn="http://"):
580 if self.isEditingCategories:
581 wizard = AddCategoryWizard(self)
584 title = wizard.getData()
585 if (not title == ''):
586 self.listing.addCategory(title)
588 wizard = AddWidgetWizard(self, self.listing, urlIn, self.listing.getListOfCategories())
591 (title, url, category) = wizard.getData()
593 self.listing.addFeed(title, url, category=category)
598 class DisplayArticle(hildon.StackableWindow):
600 A Widget for displaying an article.
602 def __init__(self, article_id, feed, feed_key, articles, config, listing):
604 article_id - The identifier of the article to load.
606 feed - The feed object containing the article (an
607 rss_sqlite:Feed object).
609 feed_key - The feed's identifier.
611 articles - A list of articles from the feed to display.
612 Needed for selecting the next/previous article (article_next).
614 config - A configuration object (config:Config).
616 listing - The listing object (rss_sqlite:Listing) that
617 contains the feed and article.
619 hildon.StackableWindow.__init__(self)
621 self.article_id = None
623 self.feed_key = feed_key
624 self.articles = articles
626 self.listing = listing
628 self.set_title(self.listing.getFeedTitle(feed_key))
630 # Init the article display
631 self.view = WebView()
632 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
633 self.view.connect("motion-notify-event", lambda w,ev: True)
634 self.view.connect('load-started', self.load_started)
635 self.view.connect('load-finished', self.load_finished)
636 self.view.connect('navigation-requested', self.navigation_requested)
637 self.view.connect("button_press_event", self.button_pressed)
638 self.gestureId = self.view.connect(
639 "button_release_event", self.button_released)
641 self.pannable_article = hildon.PannableArea()
642 self.pannable_article.add(self.view)
644 self.add(self.pannable_article)
646 self.pannable_article.show_all()
649 menu = hildon.AppMenu()
651 def menu_button(label, callback):
652 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
653 button.set_label(label)
654 button.connect("clicked", callback)
657 menu_button("Allow horizontal scrolling", self.horiz_scrolling_button)
658 menu_button("Open in browser", self.open_in_browser)
659 if feed_key == "ArchivedArticles":
661 "Remove from archived articles", self.remove_archive_button)
663 menu_button("Add to archived articles", self.archive_button)
665 self.set_app_menu(menu)
668 self.destroyId = self.connect("destroy", self.destroyWindow)
670 self.article_open(article_id)
672 def article_open(self, article_id):
674 Load the article with the specified id.
676 # If an article was open, close it.
677 if self.article_id is not None:
678 self.article_closed()
680 self.article_id = article_id
681 self.set_for_removal = False
682 self.loadedArticle = False
683 self.initial_article_load = True
685 contentLink = self.feed.getContentLink(self.article_id)
686 if contentLink.startswith("/home/user/"):
687 self.view.open("file://%s" % contentLink)
688 self.currentUrl = self.feed.getExternalLink(self.article_id)
690 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
691 self.currentUrl = str(contentLink)
693 self.feed.setEntryRead(self.article_id)
695 def article_closed(self):
697 The user has navigated away from the article. Execute any
700 if self.set_for_removal:
701 self.emit("article-deleted", self.article_id)
703 self.emit("article-closed", self.article_id)
706 def navigation_requested(self, wv, fr, req):
708 http://webkitgtk.org/reference/webkitgtk-webkitwebview.html#WebKitWebView-navigation-requested
711 fr - a WebKitWebFrame
712 req - WebKitNetworkRequest
714 if self.initial_article_load:
715 # Always initially load an article in the internal
717 self.initial_article_load = False
720 # When following a link, only use the internal browser if so
721 # configured. Otherwise, launch an external browser.
722 if self.config.getOpenInExternalBrowser():
723 self.open_in_browser(None, req.get_uri())
728 def load_started(self, *widget):
729 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
731 def load_finished(self, *widget):
732 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
733 frame = self.view.get_main_frame()
734 if self.loadedArticle:
735 self.currentUrl = frame.get_uri()
737 self.loadedArticle = True
739 def button_pressed(self, window, event):
741 The user pressed a "mouse button" (in our case, this means the
742 user likely started to drag with the finger).
744 We are only interested in whether the user performs a drag.
745 We record the starting position and when the user "releases
746 the button," we see how far the mouse moved.
748 self.coords = (event.x, event.y)
750 def button_released(self, window, event):
751 x = self.coords[0] - event.x
752 y = self.coords[1] - event.y
754 if (2*abs(y) < abs(x)):
756 self.article_next(forward=False)
758 self.article_next(forward=True)
760 # We handled the event. Don't propagate it further.
763 def article_next(self, forward=True):
765 Advance to the next (or, if forward is false, the previous)
773 id = self.feed.getNextId(id, forward)
781 if id in self.articles:
782 self.article_open(id)
785 def destroyWindow(self, *args):
786 self.article_closed()
787 self.disconnect(self.destroyId)
790 def horiz_scrolling_button(self, *widget):
791 self.pannable_article.disconnect(self.gestureId)
792 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
794 def archive_button(self, *widget):
795 # Call the listing.addArchivedArticle
796 self.listing.addArchivedArticle(self.feed_key, self.article_id)
798 def remove_archive_button(self, *widget):
799 self.set_for_removal = True
801 def open_in_browser(self, object, link=None):
803 Open the specified link using the system's browser. If not
804 link is specified, reopen the current page using the system's
808 link = self.currentUrl
810 bus = dbus.SessionBus()
811 b_proxy = bus.get_object("com.nokia.osso_browser",
812 "/com/nokia/osso_browser/request")
813 b_iface = dbus.Interface(b_proxy, 'com.nokia.osso_browser')
815 notify("Opening %s" % link)
817 # We open the link asynchronously: if the web browser is not
818 # already running, this can take a while.
821 Something went wrong opening the URL.
824 notify("Error opening %s: %s" % (link, str(exception)))
827 b_iface.open_new_window(link,
828 reply_handler=lambda *args: None,
829 error_handler=error_handler())
831 class DisplayFeed(hildon.StackableWindow):
832 def __init__(self, listing, feed, title, key, config):
833 hildon.StackableWindow.__init__(self)
834 self.listing = listing
836 self.feedTitle = title
837 self.set_title(title)
841 # If hide read articles is set, this is set to the set of
842 # unread articles at the time that feed is loaded. The last
843 # bit is important: when the user selects the next article,
844 # but then decides to move back, previous should select the
846 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 if key=="ArchivedArticles":
867 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
868 button.set_label("Delete read articles")
869 button.connect("clicked", self.buttonPurgeArticles)
872 self.set_app_menu(menu)
875 self.main_vbox = gtk.VBox(False, 0)
876 self.add(self.main_vbox)
878 self.pannableFeed = None
881 if DownloadBar.downloading ():
882 self.show_download_bar ()
884 self.connect('configure-event', self.on_configure_event)
885 self.connect("destroy", self.destroyWindow)
887 def on_configure_event(self, window, event):
888 if getattr(self, 'markup_renderer', None) is None:
891 # Fix up the column width for wrapping the text when the window is
892 # resized (i.e. orientation changed)
893 self.markup_renderer.set_property('wrap-width', event.width-20)
894 it = self.feedItems.get_iter_first()
895 while it is not None:
896 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
897 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
898 it = self.feedItems.iter_next(it)
900 def destroyWindow(self, *args):
901 #self.feed.saveUnread(CONFIGDIR)
902 self.listing.updateUnread(self.key)
903 self.emit("feed-closed", self.key)
905 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
906 #self.listing.closeCurrentlyDisplayedFeed()
908 def fix_title(self, title):
909 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
911 def displayFeed(self):
912 if self.pannableFeed:
913 self.pannableFeed.destroy()
915 self.pannableFeed = hildon.PannableArea()
917 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
919 self.feedItems = gtk.ListStore(str, str)
920 #self.feedList = gtk.TreeView(self.feedItems)
921 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
922 self.feedList.set_rules_hint(True)
924 selection = self.feedList.get_selection()
925 selection.set_mode(gtk.SELECTION_NONE)
926 #selection.connect("changed", lambda w: True)
928 self.feedList.set_model(self.feedItems)
929 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
932 self.feedList.set_hover_selection(False)
933 #self.feedList.set_property('enable-grid-lines', True)
934 #self.feedList.set_property('hildon-mode', 1)
935 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
937 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
939 vbox= gtk.VBox(False, 10)
940 vbox.pack_start(self.feedList)
942 self.pannableFeed.add_with_viewport(vbox)
944 self.markup_renderer = gtk.CellRendererText()
945 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
946 self.markup_renderer.set_property('background', bg_color) #"#333333")
947 (width, height) = self.get_size()
948 self.markup_renderer.set_property('wrap-width', width-20)
949 self.markup_renderer.set_property('ypad', 8)
950 self.markup_renderer.set_property('xpad', 5)
951 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
952 markup=FEED_COLUMN_MARKUP)
953 self.feedList.append_column(markup_column)
955 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
956 hideReadArticles = self.config.getHideReadArticles()
958 articles = self.feed.getIds(onlyUnread=True)
960 articles = self.feed.getIds()
963 self.articles[:] = []
967 isRead = self.feed.isEntryRead(id)
970 if not ( isRead and hideReadArticles ):
971 title = self.fix_title(self.feed.getTitle(id))
972 self.articles.append(id)
974 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
976 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
978 self.feedItems.append((markup, id))
981 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
983 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
984 self.feedItems.append((markup, ""))
986 self.main_vbox.pack_start(self.pannableFeed)
990 self.pannableFeed.destroy()
991 #self.remove(self.pannableFeed)
993 def on_feedList_row_activated(self, treeview, path): #, column):
994 selection = self.feedList.get_selection()
995 selection.set_mode(gtk.SELECTION_SINGLE)
996 self.feedList.get_selection().select_path(path)
997 model = treeview.get_model()
998 iter = model.get_iter(path)
999 key = model.get_value(iter, FEED_COLUMN_KEY)
1000 # Emulate legacy "button_clicked" call via treeview
1001 gobject.idle_add(self.button_clicked, treeview, key)
1004 def button_clicked(self, button, index, previous=False, next=False):
1005 newDisp = DisplayArticle(index, self.feed, self.key, self.articles, self.config, self.listing)
1006 stack = hildon.WindowStack.get_default()
1009 stack.pop_and_push(1, newDisp, tmp)
1011 gobject.timeout_add(200, self.destroyArticle, tmp)
1016 if type(self.disp).__name__ == "DisplayArticle":
1017 gobject.timeout_add(200, self.destroyArticle, self.disp)
1021 self.disp.show_all()
1024 if self.key == "ArchivedArticles":
1025 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
1026 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
1028 def buttonPurgeArticles(self, *widget):
1030 self.feed.purgeReadArticles()
1031 #self.feed.saveFeed(CONFIGDIR)
1034 def destroyArticle(self, handle):
1035 handle.destroyWindow()
1037 def mark_item_read(self, key):
1038 it = self.feedItems.get_iter_first()
1039 while it is not None:
1040 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1042 title = self.fix_title(self.feed.getTitle(key))
1043 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1044 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1046 it = self.feedItems.iter_next(it)
1048 def onArticleClosed(self, object, index):
1049 selection = self.feedList.get_selection()
1050 selection.set_mode(gtk.SELECTION_NONE)
1051 self.mark_item_read(index)
1053 def onArticleDeleted(self, object, index):
1055 self.feed.removeArticle(index)
1056 #self.feed.saveFeed(CONFIGDIR)
1060 def do_update_feed(self):
1061 self.listing.updateFeed (self.key, priority=-1)
1063 def button_update_clicked(self, button):
1064 gobject.idle_add(self.do_update_feed)
1066 def show_download_bar(self):
1067 if not type(self.downloadDialog).__name__=="DownloadBar":
1068 self.downloadDialog = DownloadBar(self.window)
1069 self.downloadDialog.connect("download-done", self.onDownloadDone)
1070 self.main_vbox.pack_end(self.downloadDialog,
1071 expand=False, fill=False)
1074 def onDownloadDone(self, widget, feed):
1075 if feed == self.feed:
1076 self.feed = self.listing.getFeed(self.key)
1080 self.downloadDialog.destroy()
1081 self.downloadDialog = False
1083 def buttonReadAllClicked(self, button):
1085 self.feed.markAllAsRead()
1086 it = self.feedItems.get_iter_first()
1087 while it is not None:
1088 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
1089 title = self.fix_title(self.feed.getTitle(k))
1090 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
1091 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
1092 it = self.feedItems.iter_next(it)
1094 #for index in self.feed.getIds():
1095 # self.feed.setEntryRead(index)
1096 # self.mark_item_read(index)
1102 self.window = hildon.StackableWindow()
1103 self.window.set_title(__appname__)
1104 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1105 self.mainVbox = gtk.VBox(False,10)
1107 if isfile(CONFIGDIR+"/feeds.db"):
1108 self.introLabel = gtk.Label("Loading...")
1110 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1112 self.mainVbox.pack_start(self.introLabel)
1114 self.window.add(self.mainVbox)
1115 self.window.show_all()
1116 self.config = Config(self.window, CONFIGDIR+"config.ini")
1117 gobject.idle_add(self.createWindow)
1119 def createWindow(self):
1121 self.listing = Listing(self.config, CONFIGDIR)
1123 self.downloadDialog = False
1125 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1126 self.orientation.set_mode(self.config.getOrientation())
1127 except Exception, e:
1128 logger.warn("Could not start rotation manager: %s" % str(e))
1130 menu = hildon.AppMenu()
1131 # Create a button and add it to the menu
1132 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1133 button.set_label("Update feeds")
1134 button.connect("clicked", self.button_update_clicked, "All")
1137 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1138 button.set_label("Mark all as read")
1139 button.connect("clicked", self.button_markAll)
1142 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1143 button.set_label("Add new feed")
1144 button.connect("clicked", lambda b: self.addFeed())
1147 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1148 button.set_label("Manage subscriptions")
1149 button.connect("clicked", self.button_organize_clicked)
1152 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1153 button.set_label("Settings")
1154 button.connect("clicked", self.button_preferences_clicked)
1157 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1158 button.set_label("About")
1159 button.connect("clicked", self.button_about_clicked)
1162 self.window.set_app_menu(menu)
1165 #self.feedWindow = hildon.StackableWindow()
1166 #self.articleWindow = hildon.StackableWindow()
1167 self.introLabel.destroy()
1168 self.pannableListing = hildon.PannableArea()
1169 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1170 self.feedList = gtk.TreeView(self.feedItems)
1171 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1172 #self.feedList.set_enable_tree_lines(True)
1173 #self.feedList.set_show_expanders(True)
1174 self.pannableListing.add(self.feedList)
1176 icon_renderer = gtk.CellRendererPixbuf()
1177 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1178 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1180 self.feedList.append_column(icon_column)
1182 markup_renderer = gtk.CellRendererText()
1183 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1184 markup=COLUMN_MARKUP)
1185 self.feedList.append_column(markup_column)
1186 self.mainVbox.pack_start(self.pannableListing)
1187 self.mainVbox.show_all()
1189 self.displayListing()
1190 self.autoupdate = False
1191 self.checkAutoUpdate()
1193 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1194 gobject.idle_add(self.late_init)
1196 def update_progress(self, percent_complete,
1197 completed, in_progress, queued,
1198 bytes_downloaded, bytes_updated, bytes_per_second,
1200 if (in_progress or queued) and not self.downloadDialog:
1201 self.downloadDialog = DownloadBar(self.window)
1202 self.downloadDialog.connect("download-done", self.onDownloadDone)
1203 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1204 self.mainVbox.show_all()
1206 if self.__dict__.get ('disp', None):
1207 self.disp.show_download_bar ()
1209 def onDownloadDone(self, widget, feed):
1211 self.downloadDialog.destroy()
1212 self.downloadDialog = False
1213 self.displayListing()
1215 def late_init(self):
1216 self.dbusHandler = ServerObject(self)
1217 bus = dbus.SessionBus()
1218 bus.add_signal_receiver(handler_function=self.update_progress,
1220 signal_name='UpdateProgress',
1221 dbus_interface='org.marcoz.feedingit',
1222 path='/org/marcoz/feedingit/update')
1224 def button_markAll(self, button):
1225 for key in self.listing.getListOfFeeds():
1226 feed = self.listing.getFeed(key)
1227 feed.markAllAsRead()
1228 #for id in feed.getIds():
1229 # feed.setEntryRead(id)
1230 self.listing.updateUnread(key)
1231 self.displayListing()
1233 def button_about_clicked(self, button):
1234 HeAboutDialog.present(self.window, \
1244 def button_export_clicked(self, button):
1245 opml = ExportOpmlData(self.window, self.listing)
1247 def button_import_clicked(self, button):
1248 opml = GetOpmlData(self.window)
1249 feeds = opml.getData()
1250 for (title, url) in feeds:
1251 self.listing.addFeed(title, url)
1252 self.displayListing()
1254 def addFeed(self, urlIn="http://"):
1255 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1258 (title, url, category) = wizard.getData()
1260 self.listing.addFeed(title, url, category=category)
1262 self.displayListing()
1264 def button_organize_clicked(self, button):
1265 def after_closing():
1266 self.displayListing()
1267 SortList(self.window, self.listing, self, after_closing)
1269 def do_update_feeds(self):
1270 for k in self.listing.getListOfFeeds():
1271 self.listing.updateFeed (k)
1273 def button_update_clicked(self, button, key):
1274 gobject.idle_add(self.do_update_feeds)
1276 def onDownloadsDone(self, *widget):
1277 self.downloadDialog.destroy()
1278 self.downloadDialog = False
1279 self.displayListing()
1281 def button_preferences_clicked(self, button):
1282 dialog = self.config.createDialog()
1283 dialog.connect("destroy", self.prefsClosed)
1285 def show_confirmation_note(self, parent, title):
1286 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1288 retcode = gtk.Dialog.run(note)
1291 if retcode == gtk.RESPONSE_OK:
1296 def saveExpandedLines(self):
1297 self.expandedLines = []
1298 model = self.feedList.get_model()
1299 model.foreach(self.checkLine)
1301 def checkLine(self, model, path, iter, data = None):
1302 if self.feedList.row_expanded(path):
1303 self.expandedLines.append(path)
1305 def restoreExpandedLines(self):
1306 model = self.feedList.get_model()
1307 model.foreach(self.restoreLine)
1309 def restoreLine(self, model, path, iter, data = None):
1310 if path in self.expandedLines:
1311 self.feedList.expand_row(path, False)
1313 def displayListing(self):
1314 icon_theme = gtk.icon_theme_get_default()
1315 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1316 gtk.ICON_LOOKUP_USE_BUILTIN)
1318 self.saveExpandedLines()
1320 self.feedItems.clear()
1321 hideReadFeed = self.config.getHideReadFeeds()
1322 order = self.config.getFeedSortOrder()
1324 categories = self.listing.getListOfCategories()
1325 if len(categories) > 1:
1326 showCategories = True
1328 showCategories = False
1330 for categoryId in categories:
1332 title = self.listing.getCategoryTitle(categoryId)
1333 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1335 if showCategories and len(keys)>0:
1336 category = self.feedItems.append(None, (None, title, categoryId))
1337 #print "catID" + str(categoryId) + " " + str(self.category)
1338 if categoryId == self.category:
1340 expandedRow = category
1343 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1344 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1345 updateTime = self.listing.getFeedUpdateTime(key)
1347 updateTime = "Never"
1348 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1350 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1352 markup = FEED_TEMPLATE % (title, subtitle)
1355 icon_filename = self.listing.getFavicon(key)
1356 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1357 LIST_ICON_SIZE, LIST_ICON_SIZE)
1359 pixbuf = default_pixbuf
1362 self.feedItems.append(category, (pixbuf, markup, key))
1364 self.feedItems.append(None, (pixbuf, markup, key))
1367 self.restoreExpandedLines()
1370 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1374 def on_feedList_row_activated(self, treeview, path, column):
1375 model = treeview.get_model()
1376 iter = model.get_iter(path)
1377 key = model.get_value(iter, COLUMN_KEY)
1380 #print "Key: " + str(key)
1382 self.category = catId
1383 if treeview.row_expanded(path):
1384 treeview.collapse_row(path)
1386 # treeview.expand_row(path, True)
1387 #treeview.collapse_all()
1388 #treeview.expand_row(path, False)
1389 #for i in range(len(path)):
1390 # self.feedList.expand_row(path[:i+1], False)
1391 #self.show_confirmation_note(self.window, "Working")
1397 def openFeed(self, key):
1399 self.disp = DisplayFeed(
1400 self.listing, self.listing.getFeed(key),
1401 self.listing.getFeedTitle(key), key,
1403 self.disp.connect("feed-closed", self.onFeedClosed)
1405 def openArticle(self, key, id):
1408 self.disp.button_clicked(None, id)
1410 def onFeedClosed(self, object, key):
1411 self.displayListing()
1413 def quit(self, *args):
1418 self.window.connect("destroy", self.quit)
1421 def prefsClosed(self, *widget):
1423 self.orientation.set_mode(self.config.getOrientation())
1426 self.displayListing()
1427 self.checkAutoUpdate()
1429 def checkAutoUpdate(self, *widget):
1430 interval = int(self.config.getUpdateInterval()*3600000)
1431 if self.config.isAutoUpdateEnabled():
1432 if self.autoupdate == False:
1433 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1434 self.autoupdate = interval
1435 elif not self.autoupdate == interval:
1436 # If auto-update is enabled, but not at the right frequency
1437 gobject.source_remove(self.autoupdateId)
1438 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1439 self.autoupdate = interval
1441 if not self.autoupdate == False:
1442 gobject.source_remove(self.autoupdateId)
1443 self.autoupdate = False
1445 def automaticUpdate(self, *widget):
1446 # Need to check for internet connection
1447 # If no internet connection, try again in 10 minutes:
1448 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1449 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1450 #from time import localtime, strftime
1451 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1453 self.button_update_clicked(None, None)
1456 def getStatus(self):
1458 for key in self.listing.getListOfFeeds():
1459 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1460 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1462 status = "No unread items"
1465 def grabFocus(self):
1466 self.window.present()
1468 if __name__ == "__main__":
1470 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1472 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1473 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1474 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1475 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1476 gobject.threads_init()
1477 if not isdir(CONFIGDIR):
1481 logger.error("Error: Can't create configuration directory")
1482 from sys import exit