1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # ============================================================================
20 __appname__ = 'FeedingIt'
21 __author__ = 'Yves Marcoz'
23 __description__ = 'A simple RSS Reader for Maemo 5'
24 # ============================================================================
27 from pango import FontDescription
32 from webkit import WebView
37 from os.path import isfile, isdir, exists
38 from os import mkdir, remove, stat
40 from aboutdialog import HeAboutDialog
41 from portrait import FremantleRotation
42 from threading import Thread, activeCount
43 from feedingitdbus import ServerObject
44 from updatedbus import UpdateServerObject, get_lock
45 from config import Config
46 from cgi import escape
48 from rss import Listing
49 from opml import GetOpmlData, ExportOpmlData
51 from urllib2 import install_opener, build_opener
53 from socket import setdefaulttimeout
55 setdefaulttimeout(timeout)
63 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
64 ABOUT_ICON = 'feedingit'
65 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
66 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
67 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
68 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
70 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
71 unread_color = color_style.lookup_color('ActiveTextColor')
72 read_color = color_style.lookup_color('DefaultTextColor')
75 CONFIGDIR="/home/user/.feedingit/"
76 LOCK = CONFIGDIR + "update.lock"
79 from htmlentitydefs import name2codepoint
81 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
83 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
87 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
88 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
89 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
91 # Build the markup template for the Maemo 5 text style
92 head_font = style.get_font_desc('SystemFont')
93 sub_font = style.get_font_desc('SmallSystemFont')
95 head_color = style.get_color('ButtonTextColor')
96 sub_color = style.get_color('DefaultTextColor')
97 active_color = style.get_color('ActiveTextColor')
99 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
100 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
102 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
103 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
105 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
106 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
108 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
109 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
111 FEED_TEMPLATE = '\n'.join((head, normal_sub))
112 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
114 ENTRY_TEMPLATE = entry_head
115 ENTRY_TEMPLATE_UNREAD = entry_active_head
118 # Removes HTML or XML character references and entities from a text string.
120 # @param text The HTML (or XML) source text.
121 # @return The plain text, as a Unicode string, if necessary.
122 # http://effbot.org/zone/re-sub.htm#unescape-html
127 # character reference
129 if text[:3] == "&#x":
130 return unichr(int(text[3:-1], 16))
132 return unichr(int(text[2:-1]))
138 text = unichr(name2codepoint[text[1:-1]])
141 return text # leave as is
142 return sub("&#?\w+;", fixup, text)
145 class AddWidgetWizard(gtk.Dialog):
146 def __init__(self, parent, urlIn, titleIn=None, isEdit=False):
147 gtk.Dialog.__init__(self)
148 self.set_transient_for(parent)
151 self.set_title('Edit RSS feed')
153 self.set_title('Add new RSS feed')
156 self.btn_add = self.add_button('Save', 2)
158 self.btn_add = self.add_button('Add', 2)
160 self.set_default_response(2)
162 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
163 self.nameEntry.set_placeholder('Feed name')
164 if not titleIn == None:
165 self.nameEntry.set_text(titleIn)
166 self.nameEntry.select_region(-1, -1)
168 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
169 self.urlEntry.set_placeholder('Feed URL')
170 self.urlEntry.set_text(urlIn)
171 self.urlEntry.select_region(-1, -1)
172 self.urlEntry.set_activates_default(True)
174 self.table = gtk.Table(2, 2, False)
175 self.table.set_col_spacings(5)
176 label = gtk.Label('Name:')
177 label.set_alignment(1., .5)
178 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
179 self.table.attach(self.nameEntry, 1, 2, 0, 1)
180 label = gtk.Label('URL:')
181 label.set_alignment(1., .5)
182 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
183 self.table.attach(self.urlEntry, 1, 2, 1, 2)
184 self.vbox.pack_start(self.table)
189 return (self.nameEntry.get_text(), self.urlEntry.get_text())
191 def some_page_func(self, nb, current, userdata):
192 # Validate data for 1st page
194 return len(self.nameEntry.get_text()) != 0
196 # Check the url is not null, and starts with http
197 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
203 class Download(Thread):
204 def __init__(self, listing, key, config):
205 Thread.__init__(self)
206 self.listing = listing
211 (use_proxy, proxy) = self.config.getProxy()
212 key_lock = get_lock(self.key)
215 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
217 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
221 class DownloadBar(gtk.ProgressBar):
222 def __init__(self, parent, listing, listOfKeys, config, single=False):
224 update_lock = get_lock("update_lock")
225 if update_lock != None:
226 gtk.ProgressBar.__init__(self)
227 self.listOfKeys = listOfKeys[:]
228 self.listing = listing
229 self.total = len(self.listOfKeys)
233 (use_proxy, proxy) = self.config.getProxy()
235 opener = build_opener(proxy)
237 opener = build_opener()
239 opener.addheaders = [('User-agent', USER_AGENT)]
240 install_opener(opener)
243 # In preparation for i18n/l10n
245 return (a if n == 1 else b)
247 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
250 self.set_fraction(self.fraction)
253 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
255 def update_progress_bar(self):
256 #self.progress_bar.pulse()
257 if activeCount() < 4:
258 x = activeCount() - 1
259 k = len(self.listOfKeys)
260 fin = self.total - k - x
261 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
262 #print x, k, fin, fraction
263 self.set_fraction(fraction)
265 if len(self.listOfKeys)>0:
266 self.current = self.current+1
267 key = self.listOfKeys.pop()
268 #if self.single == True:
269 # Check if the feed is being displayed
270 download = Download(self.listing, key, self.config)
273 elif activeCount() > 1:
276 #self.waitingWindow.destroy()
282 self.emit("download-done", "success")
287 class SortList(hildon.StackableWindow):
288 def __init__(self, parent, listing, feedingit, after_closing):
289 hildon.StackableWindow.__init__(self)
290 self.set_transient_for(parent)
291 self.set_title('Subscriptions')
292 self.listing = listing
293 self.feedingit = feedingit
294 self.after_closing = after_closing
295 self.connect('destroy', lambda w: self.after_closing())
296 self.vbox2 = gtk.VBox(False, 2)
298 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
299 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
300 button.connect("clicked", self.buttonUp)
301 self.vbox2.pack_start(button, expand=False, fill=False)
303 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
304 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
305 button.connect("clicked", self.buttonDown)
306 self.vbox2.pack_start(button, expand=False, fill=False)
308 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
310 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
311 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
312 button.connect("clicked", self.buttonAdd)
313 self.vbox2.pack_start(button, expand=False, fill=False)
315 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
316 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
317 button.connect("clicked", self.buttonEdit)
318 self.vbox2.pack_start(button, expand=False, fill=False)
320 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
321 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
322 button.connect("clicked", self.buttonDelete)
323 self.vbox2.pack_start(button, expand=False, fill=False)
325 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
326 #button.set_label("Done")
327 #button.connect("clicked", self.buttonDone)
328 #self.vbox.pack_start(button)
329 self.hbox2= gtk.HBox(False, 10)
330 self.pannableArea = hildon.PannableArea()
331 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
332 self.treeview = gtk.TreeView(self.treestore)
333 self.hbox2.pack_start(self.pannableArea, expand=True)
335 self.hbox2.pack_end(self.vbox2, expand=False)
336 self.set_default_size(-1, 600)
339 menu = hildon.AppMenu()
340 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
341 button.set_label("Import from OPML")
342 button.connect("clicked", self.feedingit.button_import_clicked)
345 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
346 button.set_label("Export to OPML")
347 button.connect("clicked", self.feedingit.button_export_clicked)
349 self.set_app_menu(menu)
353 #self.connect("destroy", self.buttonDone)
355 def displayFeeds(self):
356 self.treeview.destroy()
357 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
358 self.treeview = gtk.TreeView()
360 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
361 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
363 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
365 self.pannableArea.add(self.treeview)
369 def refreshList(self, selected=None, offset=0):
370 #rect = self.treeview.get_visible_rect()
371 #y = rect.y+rect.height
372 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
373 for key in self.listing.getListOfFeeds():
374 item = self.treestore.append([self.listing.getFeedTitle(key), key])
377 self.treeview.set_model(self.treestore)
378 if not selected == None:
379 self.treeview.get_selection().select_iter(selectedItem)
380 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
381 self.pannableArea.show_all()
383 def getSelectedItem(self):
384 (model, iter) = self.treeview.get_selection().get_selected()
387 return model.get_value(iter, 1)
389 def findIndex(self, key):
393 for row in self.treestore:
395 return (before, row.iter)
396 if key == list(row)[0]:
400 return (before, None)
402 def buttonUp(self, button):
403 key = self.getSelectedItem()
405 self.listing.moveUp(key)
406 self.refreshList(key, -10)
408 def buttonDown(self, button):
409 key = self.getSelectedItem()
411 self.listing.moveDown(key)
412 self.refreshList(key, 10)
414 def buttonDelete(self, button):
415 key = self.getSelectedItem()
417 message = 'Really remove this feed and its entries?'
418 dlg = hildon.hildon_note_new_confirmation(self, message)
421 if response == gtk.RESPONSE_OK:
422 self.listing.removeFeed(key)
425 def buttonEdit(self, button):
426 key = self.getSelectedItem()
428 if key == 'ArchivedArticles':
429 message = 'Cannot edit the archived articles feed.'
430 hildon.hildon_banner_show_information(self, '', message)
434 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
437 (title, url) = wizard.getData()
438 if (not title == '') and (not url == ''):
439 self.listing.editFeed(key, title, url)
443 def buttonDone(self, *args):
446 def buttonAdd(self, button, urlIn="http://"):
447 wizard = AddWidgetWizard(self, urlIn)
450 (title, url) = wizard.getData()
451 if (not title == '') and (not url == ''):
452 self.listing.addFeed(title, url)
457 class DisplayArticle(hildon.StackableWindow):
458 def __init__(self, feed, id, key, config, listing):
459 hildon.StackableWindow.__init__(self)
460 #self.imageDownloader = ImageDownloader()
465 #self.set_title(feed.getTitle(id))
466 self.set_title(self.listing.getFeedTitle(key))
468 self.set_for_removal = False
470 # Init the article display
471 #if self.config.getWebkitSupport():
472 self.view = WebView()
473 #self.view.set_editable(False)
476 # self.view = gtkhtml2.View()
477 # self.document = gtkhtml2.Document()
478 # self.view.set_document(self.document)
479 # self.document.connect("link_clicked", self._signal_link_clicked)
480 self.pannable_article = hildon.PannableArea()
481 self.pannable_article.add(self.view)
482 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
483 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
485 #if self.config.getWebkitSupport():
486 contentLink = self.feed.getContentLink(self.id)
487 self.feed.setEntryRead(self.id)
488 #if key=="ArchivedArticles":
489 self.loadedArticle = False
490 if contentLink.startswith("/home/user/"):
491 self.view.open("file://%s" % contentLink)
492 self.currentUrl = self.feed.getExternalLink(self.id)
494 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
495 self.currentUrl = "%s" % contentLink
496 self.view.connect("motion-notify-event", lambda w,ev: True)
497 self.view.connect('load-started', self.load_started)
498 self.view.connect('load-finished', self.load_finished)
500 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
502 menu = hildon.AppMenu()
503 # Create a button and add it to the menu
504 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
505 button.set_label("Allow horizontal scrolling")
506 button.connect("clicked", self.horiz_scrolling_button)
509 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
510 button.set_label("Open in browser")
511 button.connect("clicked", self.open_in_browser)
514 if key == "ArchivedArticles":
515 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
516 button.set_label("Remove from archived articles")
517 button.connect("clicked", self.remove_archive_button)
519 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
520 button.set_label("Add to archived articles")
521 button.connect("clicked", self.archive_button)
524 self.set_app_menu(menu)
527 self.add(self.pannable_article)
529 self.pannable_article.show_all()
531 self.destroyId = self.connect("destroy", self.destroyWindow)
533 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
534 ## Still using an old version of WebKit, so using navigation-requested signal
535 self.view.connect('navigation-requested', self.navigation_requested)
537 self.view.connect("button_press_event", self.button_pressed)
538 self.gestureId = self.view.connect("button_release_event", self.button_released)
540 #def navigation_policy_decision(self, wv, fr, req, action, decision):
541 def navigation_requested(self, wv, fr, req):
542 if self.config.getOpenInExternalBrowser():
543 self.open_in_browser(None, req.get_uri())
548 def load_started(self, *widget):
549 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
551 def load_finished(self, *widget):
552 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
553 frame = self.view.get_main_frame()
554 if self.loadedArticle:
555 self.currentUrl = frame.get_uri()
557 self.loadedArticle = True
559 def button_pressed(self, window, event):
560 #print event.x, event.y
561 self.coords = (event.x, event.y)
563 def button_released(self, window, event):
564 x = self.coords[0] - event.x
565 y = self.coords[1] - event.y
567 if (2*abs(y) < abs(x)):
569 self.emit("article-previous", self.id)
571 self.emit("article-next", self.id)
573 def destroyWindow(self, *args):
574 self.disconnect(self.destroyId)
575 if self.set_for_removal:
576 self.emit("article-deleted", self.id)
578 self.emit("article-closed", self.id)
579 #self.imageDownloader.stopAll()
582 def horiz_scrolling_button(self, *widget):
583 self.pannable_article.disconnect(self.gestureId)
584 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
586 def archive_button(self, *widget):
587 # Call the listing.addArchivedArticle
588 self.listing.addArchivedArticle(self.key, self.id)
590 def remove_archive_button(self, *widget):
591 self.set_for_removal = True
593 def open_in_browser(self, object, link=None):
595 bus = dbus.SessionBus()
596 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
597 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
599 iface.open_new_window(self.currentUrl)
601 iface.open_new_window(link)
603 class DisplayFeed(hildon.StackableWindow):
604 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
605 hildon.StackableWindow.__init__(self)
606 self.listing = listing
608 self.feedTitle = title
609 self.set_title(title)
612 self.updateDbusHandler = updateDbusHandler
614 self.downloadDialog = False
616 #self.listing.setCurrentlyDisplayedFeed(self.key)
620 menu = hildon.AppMenu()
621 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
622 button.set_label("Update feed")
623 button.connect("clicked", self.button_update_clicked)
626 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627 button.set_label("Mark all as read")
628 button.connect("clicked", self.buttonReadAllClicked)
631 if key=="ArchivedArticles":
632 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
633 button.set_label("Delete read articles")
634 button.connect("clicked", self.buttonPurgeArticles)
637 self.set_app_menu(menu)
642 self.connect('configure-event', self.on_configure_event)
643 self.connect("destroy", self.destroyWindow)
645 def on_configure_event(self, window, event):
646 if getattr(self, 'markup_renderer', None) is None:
649 # Fix up the column width for wrapping the text when the window is
650 # resized (i.e. orientation changed)
651 self.markup_renderer.set_property('wrap-width', event.width-20)
652 it = self.feedItems.get_iter_first()
653 while it is not None:
654 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
655 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
656 it = self.feedItems.iter_next(it)
658 def destroyWindow(self, *args):
659 #self.feed.saveUnread(CONFIGDIR)
660 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
661 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
662 self.emit("feed-closed", self.key)
664 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
665 #self.listing.closeCurrentlyDisplayedFeed()
667 def fix_title(self, title):
668 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
670 def displayFeed(self):
671 self.pannableFeed = hildon.PannableArea()
673 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
675 self.feedItems = gtk.ListStore(str, str)
676 #self.feedList = gtk.TreeView(self.feedItems)
677 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
678 selection = self.feedList.get_selection()
679 selection.set_mode(gtk.SELECTION_NONE)
680 #selection.connect("changed", lambda w: True)
682 self.feedList.set_model(self.feedItems)
683 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
686 self.feedList.set_hover_selection(False)
687 #self.feedList.set_property('enable-grid-lines', True)
688 #self.feedList.set_property('hildon-mode', 1)
689 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
691 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
693 vbox= gtk.VBox(False, 10)
694 vbox.pack_start(self.feedList)
696 self.pannableFeed.add_with_viewport(vbox)
698 self.markup_renderer = gtk.CellRendererText()
699 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
700 self.markup_renderer.set_property('background', "#333333")
701 (width, height) = self.get_size()
702 self.markup_renderer.set_property('wrap-width', width-20)
703 self.markup_renderer.set_property('ypad', 5)
704 self.markup_renderer.set_property('xpad', 5)
705 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
706 markup=FEED_COLUMN_MARKUP)
707 self.feedList.append_column(markup_column)
709 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
710 hideReadArticles = self.config.getHideReadArticles()
712 for id in self.feed.getIds():
715 isRead = self.feed.isEntryRead(id)
718 if not ( isRead and hideReadArticles ):
719 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
720 #title = self.feed.getTitle(id)
721 title = self.fix_title(self.feed.getTitle(id))
723 #if self.feed.isEntryRead(id):
725 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
727 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
729 self.feedItems.append((markup, id))
732 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
734 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
735 self.feedItems.append((markup, ""))
737 self.add(self.pannableFeed)
741 self.pannableFeed.destroy()
742 #self.remove(self.pannableFeed)
744 def on_feedList_row_activated(self, treeview, path): #, column):
745 selection = self.feedList.get_selection()
746 selection.set_mode(gtk.SELECTION_SINGLE)
747 self.feedList.get_selection().select_path(path)
748 model = treeview.get_model()
749 iter = model.get_iter(path)
750 key = model.get_value(iter, FEED_COLUMN_KEY)
751 # Emulate legacy "button_clicked" call via treeview
752 gobject.idle_add(self.button_clicked, treeview, key)
755 def button_clicked(self, button, index, previous=False, next=False):
756 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
757 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
758 stack = hildon.WindowStack.get_default()
761 stack.pop_and_push(1, newDisp, tmp)
763 gobject.timeout_add(200, self.destroyArticle, tmp)
768 if type(self.disp).__name__ == "DisplayArticle":
769 gobject.timeout_add(200, self.destroyArticle, self.disp)
776 if self.key == "ArchivedArticles":
777 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
778 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
779 self.ids.append(self.disp.connect("article-next", self.nextArticle))
780 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
782 def buttonPurgeArticles(self, *widget):
784 self.feed.purgeReadArticles()
785 self.feed.saveUnread(CONFIGDIR)
786 self.feed.saveFeed(CONFIGDIR)
789 def destroyArticle(self, handle):
790 handle.destroyWindow()
792 def mark_item_read(self, key):
793 it = self.feedItems.get_iter_first()
794 while it is not None:
795 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
797 title = self.fix_title(self.feed.getTitle(key))
798 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
799 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
801 it = self.feedItems.iter_next(it)
803 def nextArticle(self, object, index):
804 self.mark_item_read(index)
805 id = self.feed.getNextId(index)
806 if self.config.getHideReadArticles():
809 isRead = self.feed.isEntryRead(id)
812 while isRead and id != index:
813 id = self.feed.getNextId(id)
816 isRead = self.feed.isEntryRead(id)
820 self.button_clicked(object, id, next=True)
822 def previousArticle(self, object, index):
823 self.mark_item_read(index)
824 id = self.feed.getPreviousId(index)
825 if self.config.getHideReadArticles():
828 isRead = self.feed.isEntryRead(id)
831 while isRead and id != index:
832 id = self.feed.getPreviousId(id)
835 isRead = self.feed.isEntryRead(id)
839 self.button_clicked(object, id, previous=True)
841 def onArticleClosed(self, object, index):
842 selection = self.feedList.get_selection()
843 selection.set_mode(gtk.SELECTION_NONE)
844 self.mark_item_read(index)
846 def onArticleDeleted(self, object, index):
848 self.feed.removeArticle(index)
849 self.feed.saveUnread(CONFIGDIR)
850 self.feed.saveFeed(CONFIGDIR)
853 def button_update_clicked(self, button):
854 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
855 if not type(self.downloadDialog).__name__=="DownloadBar":
856 self.pannableFeed.destroy()
857 self.vbox = gtk.VBox(False, 10)
858 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
859 self.downloadDialog.connect("download-done", self.onDownloadsDone)
860 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
864 def onDownloadsDone(self, *widget):
866 self.feed = self.listing.getFeed(self.key)
868 self.updateDbusHandler.ArticleCountUpdated()
870 def buttonReadAllClicked(self, button):
871 for index in self.feed.getIds():
872 self.feed.setEntryRead(index)
873 self.mark_item_read(index)
879 self.window = hildon.StackableWindow()
880 self.window.set_title(__appname__)
881 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
882 self.mainVbox = gtk.VBox(False,10)
884 self.introLabel = gtk.Label("Loading...")
887 self.mainVbox.pack_start(self.introLabel)
889 self.window.add(self.mainVbox)
890 self.window.show_all()
891 self.config = Config(self.window, CONFIGDIR+"config.ini")
892 gobject.idle_add(self.createWindow)
894 def createWindow(self):
895 self.app_lock = get_lock("app_lock")
896 if self.app_lock == None:
898 self.stopButton.set_sensitive(True)
900 self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
901 self.stopButton.set_text("Stop update","")
902 self.stopButton.connect("clicked", self.stop_running_update)
903 self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
904 self.window.show_all()
905 self.introLabel.set_label("Update in progress, please wait.")
906 gobject.timeout_add_seconds(3, self.createWindow)
909 self.stopButton.destroy()
912 self.listing = Listing(CONFIGDIR)
914 self.downloadDialog = False
916 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
917 self.orientation.set_mode(self.config.getOrientation())
919 print "Could not start rotation manager"
921 menu = hildon.AppMenu()
922 # Create a button and add it to the menu
923 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
924 button.set_label("Update feeds")
925 button.connect("clicked", self.button_update_clicked, "All")
928 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
929 button.set_label("Mark all as read")
930 button.connect("clicked", self.button_markAll)
933 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
934 button.set_label("Add new feed")
935 button.connect("clicked", lambda b: self.addFeed())
938 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
939 button.set_label("Manage subscriptions")
940 button.connect("clicked", self.button_organize_clicked)
943 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
944 button.set_label("Settings")
945 button.connect("clicked", self.button_preferences_clicked)
948 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
949 button.set_label("About")
950 button.connect("clicked", self.button_about_clicked)
953 self.window.set_app_menu(menu)
956 #self.feedWindow = hildon.StackableWindow()
957 #self.articleWindow = hildon.StackableWindow()
958 self.introLabel.destroy()
959 self.pannableListing = hildon.PannableArea()
960 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
961 self.feedList = gtk.TreeView(self.feedItems)
962 self.feedList.connect('row-activated', self.on_feedList_row_activated)
963 self.pannableListing.add(self.feedList)
965 icon_renderer = gtk.CellRendererPixbuf()
966 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
967 icon_column = gtk.TreeViewColumn('', icon_renderer, \
969 self.feedList.append_column(icon_column)
971 markup_renderer = gtk.CellRendererText()
972 markup_column = gtk.TreeViewColumn('', markup_renderer, \
973 markup=COLUMN_MARKUP)
974 self.feedList.append_column(markup_column)
975 self.mainVbox.pack_start(self.pannableListing)
976 self.mainVbox.show_all()
978 self.displayListing()
979 self.autoupdate = False
980 self.checkAutoUpdate()
981 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
982 gobject.idle_add(self.enableDbus)
984 def stop_running_update(self, button):
985 self.stopButton.set_sensitive(False)
987 bus=dbus.SessionBus()
988 remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
989 "/org/marcoz/feedingit/update" # Object's path
991 iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
994 def enableDbus(self):
995 self.dbusHandler = ServerObject(self)
996 self.updateDbusHandler = UpdateServerObject(self)
998 def button_markAll(self, button):
999 for key in self.listing.getListOfFeeds():
1000 feed = self.listing.getFeed(key)
1001 for id in feed.getIds():
1002 feed.setEntryRead(id)
1003 feed.saveUnread(CONFIGDIR)
1004 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
1005 self.displayListing()
1007 def button_about_clicked(self, button):
1008 HeAboutDialog.present(self.window, \
1018 def button_export_clicked(self, button):
1019 opml = ExportOpmlData(self.window, self.listing)
1021 def button_import_clicked(self, button):
1022 opml = GetOpmlData(self.window)
1023 feeds = opml.getData()
1024 for (title, url) in feeds:
1025 self.listing.addFeed(title, url)
1026 self.displayListing()
1028 def addFeed(self, urlIn="http://"):
1029 wizard = AddWidgetWizard(self.window, urlIn)
1032 (title, url) = wizard.getData()
1033 if (not title == '') and (not url == ''):
1034 self.listing.addFeed(title, url)
1036 self.displayListing()
1038 def button_organize_clicked(self, button):
1039 def after_closing():
1040 self.listing.saveConfig()
1041 self.displayListing()
1042 SortList(self.window, self.listing, self, after_closing)
1044 def button_update_clicked(self, button, key):
1045 if not type(self.downloadDialog).__name__=="DownloadBar":
1046 self.updateDbusHandler.UpdateStarted()
1047 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1048 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1049 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1050 self.mainVbox.show_all()
1051 #self.displayListing()
1053 def onDownloadsDone(self, *widget):
1054 self.downloadDialog.destroy()
1055 self.downloadDialog = False
1056 self.displayListing()
1057 self.updateDbusHandler.UpdateFinished()
1058 self.updateDbusHandler.ArticleCountUpdated()
1060 def button_preferences_clicked(self, button):
1061 dialog = self.config.createDialog()
1062 dialog.connect("destroy", self.prefsClosed)
1064 def show_confirmation_note(self, parent, title):
1065 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1067 retcode = gtk.Dialog.run(note)
1070 if retcode == gtk.RESPONSE_OK:
1075 def displayListing(self):
1076 icon_theme = gtk.icon_theme_get_default()
1077 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1078 gtk.ICON_LOOKUP_USE_BUILTIN)
1080 self.feedItems.clear()
1083 for key in self.listing.getListOfFeeds():
1084 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1085 if unreadItems > 0 or not self.config.getHideReadFeeds():
1087 title = self.listing.getFeedTitle(key)
1088 updateTime = self.listing.getFeedUpdateTime(key)
1089 updateStamp = self.listing.getFeedUpdateStamp(key)
1090 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1091 feedInfo[key] = [count, unreadItems, updateStamp, title, subtitle, updateTime];
1093 order = self.config.getFeedSortOrder();
1094 if order == "Most unread":
1095 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
1096 elif order == "Least unread":
1097 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
1098 elif order == "Most recent":
1099 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
1100 elif order == "Least recent":
1101 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
1102 else: # order == "Manual" or invalid value...
1103 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
1105 for key in keyorder:
1106 unreadItems = feedInfo[key][1]
1107 title = xml.sax.saxutils.escape(feedInfo[key][3])
1108 subtitle = feedInfo[key][4]
1109 updateTime = feedInfo[key][5]
1111 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1113 markup = FEED_TEMPLATE % (title, subtitle)
1116 icon_filename = self.listing.getFavicon(key)
1117 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1118 LIST_ICON_SIZE, LIST_ICON_SIZE)
1120 pixbuf = default_pixbuf
1122 self.feedItems.append((pixbuf, markup, key))
1124 def on_feedList_row_activated(self, treeview, path, column):
1125 model = treeview.get_model()
1126 iter = model.get_iter(path)
1127 key = model.get_value(iter, COLUMN_KEY)
1130 def openFeed(self, key):
1134 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1136 self.feed_lock = get_lock(key)
1137 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1138 self.listing.getFeedTitle(key), key, \
1139 self.config, self.updateDbusHandler)
1140 self.disp.connect("feed-closed", self.onFeedClosed)
1143 def onFeedClosed(self, object, key):
1144 #self.listing.saveConfig()
1146 gobject.idle_add(self.onFeedClosedTimeout)
1147 self.displayListing()
1148 #self.updateDbusHandler.ArticleCountUpdated()
1150 def onFeedClosedTimeout(self):
1151 self.listing.saveConfig()
1153 self.updateDbusHandler.ArticleCountUpdated()
1156 self.window.connect("destroy", gtk.main_quit)
1158 self.listing.saveConfig()
1161 def prefsClosed(self, *widget):
1163 self.orientation.set_mode(self.config.getOrientation())
1166 self.displayListing()
1167 self.checkAutoUpdate()
1169 def checkAutoUpdate(self, *widget):
1170 interval = int(self.config.getUpdateInterval()*3600000)
1171 if self.config.isAutoUpdateEnabled():
1172 if self.autoupdate == False:
1173 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1174 self.autoupdate = interval
1175 elif not self.autoupdate == interval:
1176 # If auto-update is enabled, but not at the right frequency
1177 gobject.source_remove(self.autoupdateId)
1178 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1179 self.autoupdate = interval
1181 if not self.autoupdate == False:
1182 gobject.source_remove(self.autoupdateId)
1183 self.autoupdate = False
1185 def automaticUpdate(self, *widget):
1186 # Need to check for internet connection
1187 # If no internet connection, try again in 10 minutes:
1188 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1189 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1190 #from time import localtime, strftime
1191 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1193 self.button_update_clicked(None, None)
1196 def stopUpdate(self):
1197 # Not implemented in the app (see update_feeds.py)
1199 self.downloadDialog.listOfKeys = []
1203 def getStatus(self):
1205 for key in self.listing.getListOfFeeds():
1206 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1207 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1209 status = "No unread items"
1212 if __name__ == "__main__":
1213 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1214 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1215 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1216 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1217 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1218 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1219 gobject.threads_init()
1220 if not isdir(CONFIGDIR):
1224 print "Error: Can't create configuration directory"
1225 from sys import exit