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 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
1105 self.config = Config(self.window, CONFIGDIR+"config.ini")
1108 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
1109 self.orientation.set_mode(self.config.getOrientation())
1110 except Exception, e:
1111 logger.warn("Could not start rotation manager: %s" % str(e))
1113 self.window.set_title(__appname__)
1114 self.mainVbox = gtk.VBox(False,10)
1116 if isfile(CONFIGDIR+"/feeds.db"):
1117 self.introLabel = gtk.Label("Loading...")
1119 self.introLabel = gtk.Label("Updating database to new format...\nThis can take several minutes.")
1121 self.mainVbox.pack_start(self.introLabel)
1123 self.window.add(self.mainVbox)
1124 self.window.show_all()
1125 gobject.idle_add(self.createWindow)
1127 def createWindow(self):
1129 self.listing = Listing(self.config, CONFIGDIR)
1131 self.downloadDialog = False
1133 menu = hildon.AppMenu()
1134 # Create a button and add it to the menu
1135 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1136 button.set_label("Update feeds")
1137 button.connect("clicked", self.button_update_clicked, "All")
1140 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1141 button.set_label("Mark all as read")
1142 button.connect("clicked", self.button_markAll)
1145 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1146 button.set_label("Add new feed")
1147 button.connect("clicked", lambda b: self.addFeed())
1150 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1151 button.set_label("Manage subscriptions")
1152 button.connect("clicked", self.button_organize_clicked)
1155 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1156 button.set_label("Settings")
1157 button.connect("clicked", self.button_preferences_clicked)
1160 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
1161 button.set_label("About")
1162 button.connect("clicked", self.button_about_clicked)
1165 self.window.set_app_menu(menu)
1168 #self.feedWindow = hildon.StackableWindow()
1169 #self.articleWindow = hildon.StackableWindow()
1170 self.introLabel.destroy()
1171 self.pannableListing = hildon.PannableArea()
1172 self.feedItems = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
1173 self.feedList = gtk.TreeView(self.feedItems)
1174 self.feedList.connect('row-activated', self.on_feedList_row_activated)
1175 #self.feedList.set_enable_tree_lines(True)
1176 #self.feedList.set_show_expanders(True)
1177 self.pannableListing.add(self.feedList)
1179 icon_renderer = gtk.CellRendererPixbuf()
1180 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
1181 icon_column = gtk.TreeViewColumn('', icon_renderer, \
1183 self.feedList.append_column(icon_column)
1185 markup_renderer = gtk.CellRendererText()
1186 markup_column = gtk.TreeViewColumn('', markup_renderer, \
1187 markup=COLUMN_MARKUP)
1188 self.feedList.append_column(markup_column)
1189 self.mainVbox.pack_start(self.pannableListing)
1190 self.mainVbox.show_all()
1192 self.displayListing()
1193 self.autoupdate = False
1194 self.checkAutoUpdate()
1196 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
1197 gobject.idle_add(self.late_init)
1199 def update_progress(self, percent_complete,
1200 completed, in_progress, queued,
1201 bytes_downloaded, bytes_updated, bytes_per_second,
1203 if (in_progress or queued) and not self.downloadDialog:
1204 self.downloadDialog = DownloadBar(self.window)
1205 self.downloadDialog.connect("download-done", self.onDownloadDone)
1206 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1207 self.mainVbox.show_all()
1209 if self.__dict__.get ('disp', None):
1210 self.disp.show_download_bar ()
1212 def onDownloadDone(self, widget, feed):
1214 self.downloadDialog.destroy()
1215 self.downloadDialog = False
1216 self.displayListing()
1218 def late_init(self):
1219 self.dbusHandler = ServerObject(self)
1220 bus = dbus.SessionBus()
1221 bus.add_signal_receiver(handler_function=self.update_progress,
1223 signal_name='UpdateProgress',
1224 dbus_interface='org.marcoz.feedingit',
1225 path='/org/marcoz/feedingit/update')
1227 def button_markAll(self, button):
1228 for key in self.listing.getListOfFeeds():
1229 feed = self.listing.getFeed(key)
1230 feed.markAllAsRead()
1231 #for id in feed.getIds():
1232 # feed.setEntryRead(id)
1233 self.listing.updateUnread(key)
1234 self.displayListing()
1236 def button_about_clicked(self, button):
1237 HeAboutDialog.present(self.window, \
1247 def button_export_clicked(self, button):
1248 opml = ExportOpmlData(self.window, self.listing)
1250 def button_import_clicked(self, button):
1251 opml = GetOpmlData(self.window)
1252 feeds = opml.getData()
1253 for (title, url) in feeds:
1254 self.listing.addFeed(title, url)
1255 self.displayListing()
1257 def addFeed(self, urlIn="http://"):
1258 wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
1261 (title, url, category) = wizard.getData()
1263 self.listing.addFeed(title, url, category=category)
1265 self.displayListing()
1267 def button_organize_clicked(self, button):
1268 def after_closing():
1269 self.displayListing()
1270 SortList(self.window, self.listing, self, after_closing)
1272 def do_update_feeds(self):
1273 for k in self.listing.getListOfFeeds():
1274 self.listing.updateFeed (k)
1276 def button_update_clicked(self, button, key):
1277 gobject.idle_add(self.do_update_feeds)
1279 def onDownloadsDone(self, *widget):
1280 self.downloadDialog.destroy()
1281 self.downloadDialog = False
1282 self.displayListing()
1284 def button_preferences_clicked(self, button):
1285 dialog = self.config.createDialog()
1286 dialog.connect("destroy", self.prefsClosed)
1288 def show_confirmation_note(self, parent, title):
1289 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1291 retcode = gtk.Dialog.run(note)
1294 if retcode == gtk.RESPONSE_OK:
1299 def saveExpandedLines(self):
1300 self.expandedLines = []
1301 model = self.feedList.get_model()
1302 model.foreach(self.checkLine)
1304 def checkLine(self, model, path, iter, data = None):
1305 if self.feedList.row_expanded(path):
1306 self.expandedLines.append(path)
1308 def restoreExpandedLines(self):
1309 model = self.feedList.get_model()
1310 model.foreach(self.restoreLine)
1312 def restoreLine(self, model, path, iter, data = None):
1313 if path in self.expandedLines:
1314 self.feedList.expand_row(path, False)
1316 def displayListing(self):
1317 icon_theme = gtk.icon_theme_get_default()
1318 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1319 gtk.ICON_LOOKUP_USE_BUILTIN)
1321 self.saveExpandedLines()
1323 self.feedItems.clear()
1324 hideReadFeed = self.config.getHideReadFeeds()
1325 order = self.config.getFeedSortOrder()
1327 categories = self.listing.getListOfCategories()
1328 if len(categories) > 1:
1329 showCategories = True
1331 showCategories = False
1333 for categoryId in categories:
1335 title = self.listing.getCategoryTitle(categoryId)
1336 keys = self.listing.getSortedListOfKeys(order, onlyUnread=hideReadFeed, category=categoryId)
1338 if showCategories and len(keys)>0:
1339 category = self.feedItems.append(None, (None, title, categoryId))
1340 #print "catID" + str(categoryId) + " " + str(self.category)
1341 if categoryId == self.category:
1343 expandedRow = category
1346 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1347 title = xml.sax.saxutils.escape(self.listing.getFeedTitle(key))
1348 updateTime = self.listing.getFeedUpdateTime(key)
1349 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1351 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1353 markup = FEED_TEMPLATE % (title, subtitle)
1356 icon_filename = self.listing.getFavicon(key)
1357 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1358 LIST_ICON_SIZE, LIST_ICON_SIZE)
1360 pixbuf = default_pixbuf
1363 self.feedItems.append(category, (pixbuf, markup, key))
1365 self.feedItems.append(None, (pixbuf, markup, key))
1368 self.restoreExpandedLines()
1371 # self.feedList.expand_row(self.feeItems.get_path(expandedRow), True)
1375 def on_feedList_row_activated(self, treeview, path, column):
1376 model = treeview.get_model()
1377 iter = model.get_iter(path)
1378 key = model.get_value(iter, COLUMN_KEY)
1381 #print "Key: " + str(key)
1383 self.category = catId
1384 if treeview.row_expanded(path):
1385 treeview.collapse_row(path)
1387 # treeview.expand_row(path, True)
1388 #treeview.collapse_all()
1389 #treeview.expand_row(path, False)
1390 #for i in range(len(path)):
1391 # self.feedList.expand_row(path[:i+1], False)
1392 #self.show_confirmation_note(self.window, "Working")
1398 def openFeed(self, key):
1400 self.disp = DisplayFeed(
1401 self.listing, self.listing.getFeed(key),
1402 self.listing.getFeedTitle(key), key,
1404 self.disp.connect("feed-closed", self.onFeedClosed)
1406 def openArticle(self, key, id):
1409 self.disp.button_clicked(None, id)
1411 def onFeedClosed(self, object, key):
1412 self.displayListing()
1414 def quit(self, *args):
1419 self.window.connect("destroy", self.quit)
1422 def prefsClosed(self, *widget):
1424 self.orientation.set_mode(self.config.getOrientation())
1427 self.displayListing()
1428 self.checkAutoUpdate()
1430 def checkAutoUpdate(self, *widget):
1431 interval = int(self.config.getUpdateInterval()*3600000)
1432 if self.config.isAutoUpdateEnabled():
1433 if self.autoupdate == False:
1434 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1435 self.autoupdate = interval
1436 elif not self.autoupdate == interval:
1437 # If auto-update is enabled, but not at the right frequency
1438 gobject.source_remove(self.autoupdateId)
1439 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1440 self.autoupdate = interval
1442 if not self.autoupdate == False:
1443 gobject.source_remove(self.autoupdateId)
1444 self.autoupdate = False
1446 def automaticUpdate(self, *widget):
1447 # Need to check for internet connection
1448 # If no internet connection, try again in 10 minutes:
1449 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1450 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1451 #from time import localtime, strftime
1452 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1454 self.button_update_clicked(None, None)
1457 def getStatus(self):
1459 for key in self.listing.getListOfFeeds():
1460 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1461 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1463 status = "No unread items"
1466 def grabFocus(self):
1467 self.window.present()
1469 if __name__ == "__main__":
1471 debugging.init(dot_directory=".feedingit", program_name="feedingit")
1473 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1474 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1475 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1476 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1477 gobject.threads_init()
1478 if not isdir(CONFIGDIR):
1482 logger.error("Error: Can't create configuration directory")
1483 from sys import exit