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)
61 USER_AGENT = 'Mozilla/5.0 (compatible; Maemo 5;) %s %s' % (__appname__, __version__)
62 ABOUT_ICON = 'feedingit'
63 ABOUT_COPYRIGHT = 'Copyright (c) 2010 %s' % __author__
64 ABOUT_WEBSITE = 'http://feedingit.marcoz.org/'
65 ABOUT_BUGTRACKER = 'https://garage.maemo.org/tracker/?group_id=1202'
66 ABOUT_DONATE = None # TODO: Create a donation page + add its URL here
68 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
69 unread_color = color_style.lookup_color('ActiveTextColor')
70 read_color = color_style.lookup_color('DefaultTextColor')
73 CONFIGDIR="/home/user/.feedingit/"
74 LOCK = CONFIGDIR + "update.lock"
77 from htmlentitydefs import name2codepoint
79 COLUMN_ICON, COLUMN_MARKUP, COLUMN_KEY = range(3)
81 FEED_COLUMN_MARKUP, FEED_COLUMN_KEY = range(2)
85 MARKUP_TEMPLATE= '<span font_desc="%s" foreground="%s">%%s</span>'
86 MARKUP_TEMPLATE_ENTRY_UNREAD = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
87 MARKUP_TEMPLATE_ENTRY = '<span font_desc="%s italic %%s" foreground="%s">%%s</span>'
89 # Build the markup template for the Maemo 5 text style
90 head_font = style.get_font_desc('SystemFont')
91 sub_font = style.get_font_desc('SmallSystemFont')
93 head_color = style.get_color('ButtonTextColor')
94 sub_color = style.get_color('DefaultTextColor')
95 active_color = style.get_color('ActiveTextColor')
97 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
98 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
100 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
101 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
103 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
104 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
106 entry_active_head = MARKUP_TEMPLATE_ENTRY_UNREAD % (head_font.get_family(), active_color.to_string())
107 entry_active_sub = MARKUP_TEMPLATE_ENTRY_UNREAD % (sub_font.get_family(), active_color.to_string())
109 FEED_TEMPLATE = '\n'.join((head, normal_sub))
110 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
112 ENTRY_TEMPLATE = entry_head
113 ENTRY_TEMPLATE_UNREAD = entry_active_head
116 # Removes HTML or XML character references and entities from a text string.
118 # @param text The HTML (or XML) source text.
119 # @return The plain text, as a Unicode string, if necessary.
120 # http://effbot.org/zone/re-sub.htm#unescape-html
125 # character reference
127 if text[:3] == "&#x":
128 return unichr(int(text[3:-1], 16))
130 return unichr(int(text[2:-1]))
136 text = unichr(name2codepoint[text[1:-1]])
139 return text # leave as is
140 return sub("&#?\w+;", fixup, text)
143 class AddWidgetWizard(gtk.Dialog):
144 def __init__(self, parent, urlIn, titleIn=None, isEdit=False):
145 gtk.Dialog.__init__(self)
146 self.set_transient_for(parent)
149 self.set_title('Edit RSS feed')
151 self.set_title('Add new RSS feed')
154 self.btn_add = self.add_button('Save', 2)
156 self.btn_add = self.add_button('Add', 2)
158 self.set_default_response(2)
160 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
161 self.nameEntry.set_placeholder('Feed name')
162 if not titleIn == None:
163 self.nameEntry.set_text(titleIn)
164 self.nameEntry.select_region(-1, -1)
166 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
167 self.urlEntry.set_placeholder('Feed URL')
168 self.urlEntry.set_text(urlIn)
169 self.urlEntry.select_region(-1, -1)
170 self.urlEntry.set_activates_default(True)
172 self.table = gtk.Table(2, 2, False)
173 self.table.set_col_spacings(5)
174 label = gtk.Label('Name:')
175 label.set_alignment(1., .5)
176 self.table.attach(label, 0, 1, 0, 1, gtk.FILL)
177 self.table.attach(self.nameEntry, 1, 2, 0, 1)
178 label = gtk.Label('URL:')
179 label.set_alignment(1., .5)
180 self.table.attach(label, 0, 1, 1, 2, gtk.FILL)
181 self.table.attach(self.urlEntry, 1, 2, 1, 2)
182 self.vbox.pack_start(self.table)
187 return (self.nameEntry.get_text(), self.urlEntry.get_text())
189 def some_page_func(self, nb, current, userdata):
190 # Validate data for 1st page
192 return len(self.nameEntry.get_text()) != 0
194 # Check the url is not null, and starts with http
195 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
201 class Download(Thread):
202 def __init__(self, listing, key, config):
203 Thread.__init__(self)
204 self.listing = listing
209 (use_proxy, proxy) = self.config.getProxy()
210 key_lock = get_lock(self.key)
213 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
215 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
219 class DownloadBar(gtk.ProgressBar):
220 def __init__(self, parent, listing, listOfKeys, config, single=False):
222 update_lock = get_lock("update_lock")
223 if update_lock != None:
224 gtk.ProgressBar.__init__(self)
225 self.listOfKeys = listOfKeys[:]
226 self.listing = listing
227 self.total = len(self.listOfKeys)
231 (use_proxy, proxy) = self.config.getProxy()
233 opener = build_opener(proxy)
235 opener = build_opener()
237 opener.addheaders = [('User-agent', USER_AGENT)]
238 install_opener(opener)
241 # In preparation for i18n/l10n
243 return (a if n == 1 else b)
245 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
248 self.set_fraction(self.fraction)
251 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
253 def update_progress_bar(self):
254 #self.progress_bar.pulse()
255 if activeCount() < 4:
256 x = activeCount() - 1
257 k = len(self.listOfKeys)
258 fin = self.total - k - x
259 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
260 #print x, k, fin, fraction
261 self.set_fraction(fraction)
263 if len(self.listOfKeys)>0:
264 self.current = self.current+1
265 key = self.listOfKeys.pop()
266 #if self.single == True:
267 # Check if the feed is being displayed
268 download = Download(self.listing, key, self.config)
271 elif activeCount() > 1:
274 #self.waitingWindow.destroy()
280 self.emit("download-done", "success")
285 class SortList(hildon.StackableWindow):
286 def __init__(self, parent, listing, feedingit, after_closing):
287 hildon.StackableWindow.__init__(self)
288 self.set_transient_for(parent)
289 self.set_title('Subscriptions')
290 self.listing = listing
291 self.feedingit = feedingit
292 self.after_closing = after_closing
293 self.connect('destroy', lambda w: self.after_closing())
294 self.vbox2 = gtk.VBox(False, 2)
296 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
297 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
298 button.connect("clicked", self.buttonUp)
299 self.vbox2.pack_start(button, expand=False, fill=False)
301 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
302 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
303 button.connect("clicked", self.buttonDown)
304 self.vbox2.pack_start(button, expand=False, fill=False)
306 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
308 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
309 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
310 button.connect("clicked", self.buttonAdd)
311 self.vbox2.pack_start(button, expand=False, fill=False)
313 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
314 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
315 button.connect("clicked", self.buttonEdit)
316 self.vbox2.pack_start(button, expand=False, fill=False)
318 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
319 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
320 button.connect("clicked", self.buttonDelete)
321 self.vbox2.pack_start(button, expand=False, fill=False)
323 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
324 #button.set_label("Done")
325 #button.connect("clicked", self.buttonDone)
326 #self.vbox.pack_start(button)
327 self.hbox2= gtk.HBox(False, 10)
328 self.pannableArea = hildon.PannableArea()
329 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
330 self.treeview = gtk.TreeView(self.treestore)
331 self.hbox2.pack_start(self.pannableArea, expand=True)
333 self.hbox2.pack_end(self.vbox2, expand=False)
334 self.set_default_size(-1, 600)
337 menu = hildon.AppMenu()
338 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
339 button.set_label("Import from OPML")
340 button.connect("clicked", self.feedingit.button_import_clicked)
343 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
344 button.set_label("Export to OPML")
345 button.connect("clicked", self.feedingit.button_export_clicked)
347 self.set_app_menu(menu)
351 #self.connect("destroy", self.buttonDone)
353 def displayFeeds(self):
354 self.treeview.destroy()
355 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
356 self.treeview = gtk.TreeView()
358 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
359 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
361 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
363 self.pannableArea.add(self.treeview)
367 def refreshList(self, selected=None, offset=0):
368 #rect = self.treeview.get_visible_rect()
369 #y = rect.y+rect.height
370 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
371 for key in self.listing.getListOfFeeds():
372 item = self.treestore.append([self.listing.getFeedTitle(key), key])
375 self.treeview.set_model(self.treestore)
376 if not selected == None:
377 self.treeview.get_selection().select_iter(selectedItem)
378 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
379 self.pannableArea.show_all()
381 def getSelectedItem(self):
382 (model, iter) = self.treeview.get_selection().get_selected()
385 return model.get_value(iter, 1)
387 def findIndex(self, key):
391 for row in self.treestore:
393 return (before, row.iter)
394 if key == list(row)[0]:
398 return (before, None)
400 def buttonUp(self, button):
401 key = self.getSelectedItem()
403 self.listing.moveUp(key)
404 self.refreshList(key, -10)
406 def buttonDown(self, button):
407 key = self.getSelectedItem()
409 self.listing.moveDown(key)
410 self.refreshList(key, 10)
412 def buttonDelete(self, button):
413 key = self.getSelectedItem()
415 message = 'Really remove this feed and its entries?'
416 dlg = hildon.hildon_note_new_confirmation(self, message)
419 if response == gtk.RESPONSE_OK:
420 self.listing.removeFeed(key)
423 def buttonEdit(self, button):
424 key = self.getSelectedItem()
426 if key == 'ArchivedArticles':
427 message = 'Cannot edit the archived articles feed.'
428 hildon.hildon_banner_show_information(self, '', message)
432 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
435 (title, url) = wizard.getData()
436 if (not title == '') and (not url == ''):
437 self.listing.editFeed(key, title, url)
441 def buttonDone(self, *args):
444 def buttonAdd(self, button, urlIn="http://"):
445 wizard = AddWidgetWizard(self, urlIn)
448 (title, url) = wizard.getData()
449 if (not title == '') and (not url == ''):
450 self.listing.addFeed(title, url)
455 class DisplayArticle(hildon.StackableWindow):
456 def __init__(self, feed, id, key, config, listing):
457 hildon.StackableWindow.__init__(self)
458 #self.imageDownloader = ImageDownloader()
463 #self.set_title(feed.getTitle(id))
464 self.set_title(self.listing.getFeedTitle(key))
466 self.set_for_removal = False
468 # Init the article display
469 #if self.config.getWebkitSupport():
470 self.view = WebView()
471 #self.view.set_editable(False)
474 # self.view = gtkhtml2.View()
475 # self.document = gtkhtml2.Document()
476 # self.view.set_document(self.document)
477 # self.document.connect("link_clicked", self._signal_link_clicked)
478 self.pannable_article = hildon.PannableArea()
479 self.pannable_article.add(self.view)
480 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
481 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
483 #if self.config.getWebkitSupport():
484 contentLink = self.feed.getContentLink(self.id)
485 self.feed.setEntryRead(self.id)
486 #if key=="ArchivedArticles":
487 self.loadedArticle = False
488 if contentLink.startswith("/home/user/"):
489 self.view.open("file://%s" % contentLink)
490 self.currentUrl = self.feed.getExternalLink(self.id)
492 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
493 self.currentUrl = "%s" % contentLink
494 self.view.connect("motion-notify-event", lambda w,ev: True)
495 self.view.connect('load-started', self.load_started)
496 self.view.connect('load-finished', self.load_finished)
498 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
500 menu = hildon.AppMenu()
501 # Create a button and add it to the menu
502 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
503 button.set_label("Allow horizontal scrolling")
504 button.connect("clicked", self.horiz_scrolling_button)
507 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
508 button.set_label("Open in browser")
509 button.connect("clicked", self.open_in_browser)
512 if key == "ArchivedArticles":
513 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
514 button.set_label("Remove from archived articles")
515 button.connect("clicked", self.remove_archive_button)
517 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
518 button.set_label("Add to archived articles")
519 button.connect("clicked", self.archive_button)
522 self.set_app_menu(menu)
525 self.add(self.pannable_article)
527 self.pannable_article.show_all()
529 self.destroyId = self.connect("destroy", self.destroyWindow)
531 #self.view.connect('navigation-policy-decision-requested', self.navigation_policy_decision)
532 ## Still using an old version of WebKit, so using navigation-requested signal
533 self.view.connect('navigation-requested', self.navigation_requested)
535 self.view.connect("button_press_event", self.button_pressed)
536 self.gestureId = self.view.connect("button_release_event", self.button_released)
538 #def navigation_policy_decision(self, wv, fr, req, action, decision):
539 def navigation_requested(self, wv, fr, req):
540 if self.config.getOpenInExternalBrowser():
541 self.open_in_browser(None, req.get_uri())
546 def load_started(self, *widget):
547 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
549 def load_finished(self, *widget):
550 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
551 frame = self.view.get_main_frame()
552 if self.loadedArticle:
553 self.currentUrl = frame.get_uri()
555 self.loadedArticle = True
557 def button_pressed(self, window, event):
558 #print event.x, event.y
559 self.coords = (event.x, event.y)
561 def button_released(self, window, event):
562 x = self.coords[0] - event.x
563 y = self.coords[1] - event.y
565 if (2*abs(y) < abs(x)):
567 self.emit("article-previous", self.id)
569 self.emit("article-next", self.id)
571 def destroyWindow(self, *args):
572 self.disconnect(self.destroyId)
573 if self.set_for_removal:
574 self.emit("article-deleted", self.id)
576 self.emit("article-closed", self.id)
577 #self.imageDownloader.stopAll()
580 def horiz_scrolling_button(self, *widget):
581 self.pannable_article.disconnect(self.gestureId)
582 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
584 def archive_button(self, *widget):
585 # Call the listing.addArchivedArticle
586 self.listing.addArchivedArticle(self.key, self.id)
588 def remove_archive_button(self, *widget):
589 self.set_for_removal = True
591 def open_in_browser(self, object, link=None):
593 bus = dbus.SessionBus()
594 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
595 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
597 iface.open_new_window(self.currentUrl)
599 iface.open_new_window(link)
601 class DisplayFeed(hildon.StackableWindow):
602 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
603 hildon.StackableWindow.__init__(self)
604 self.listing = listing
606 self.feedTitle = title
607 self.set_title(title)
610 self.updateDbusHandler = updateDbusHandler
612 self.downloadDialog = False
614 #self.listing.setCurrentlyDisplayedFeed(self.key)
618 menu = hildon.AppMenu()
619 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
620 button.set_label("Update feed")
621 button.connect("clicked", self.button_update_clicked)
624 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
625 button.set_label("Mark all as read")
626 button.connect("clicked", self.buttonReadAllClicked)
629 if key=="ArchivedArticles":
630 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631 button.set_label("Delete read articles")
632 button.connect("clicked", self.buttonPurgeArticles)
635 self.set_app_menu(menu)
640 self.connect('configure-event', self.on_configure_event)
641 self.connect("destroy", self.destroyWindow)
643 def on_configure_event(self, window, event):
644 if getattr(self, 'markup_renderer', None) is None:
647 # Fix up the column width for wrapping the text when the window is
648 # resized (i.e. orientation changed)
649 self.markup_renderer.set_property('wrap-width', event.width-20)
650 it = self.feedItems.get_iter_first()
651 while it is not None:
652 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
653 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
654 it = self.feedItems.iter_next(it)
656 def destroyWindow(self, *args):
657 #self.feed.saveUnread(CONFIGDIR)
658 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
659 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
660 self.emit("feed-closed", self.key)
662 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
663 #self.listing.closeCurrentlyDisplayedFeed()
665 def fix_title(self, title):
666 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
668 def displayFeed(self):
669 self.pannableFeed = hildon.PannableArea()
671 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
673 self.feedItems = gtk.ListStore(str, str)
674 #self.feedList = gtk.TreeView(self.feedItems)
675 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
676 selection = self.feedList.get_selection()
677 selection.set_mode(gtk.SELECTION_NONE)
678 #selection.connect("changed", lambda w: True)
680 self.feedList.set_model(self.feedItems)
681 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
684 self.feedList.set_hover_selection(False)
685 #self.feedList.set_property('enable-grid-lines', True)
686 #self.feedList.set_property('hildon-mode', 1)
687 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
689 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
691 vbox= gtk.VBox(False, 10)
692 vbox.pack_start(self.feedList)
694 self.pannableFeed.add_with_viewport(vbox)
696 self.markup_renderer = gtk.CellRendererText()
697 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
698 self.markup_renderer.set_property('background', "#333333")
699 (width, height) = self.get_size()
700 self.markup_renderer.set_property('wrap-width', width-20)
701 self.markup_renderer.set_property('ypad', 5)
702 self.markup_renderer.set_property('xpad', 5)
703 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
704 markup=FEED_COLUMN_MARKUP)
705 self.feedList.append_column(markup_column)
707 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
708 hideReadArticles = self.config.getHideReadArticles()
710 for id in self.feed.getIds():
713 isRead = self.feed.isEntryRead(id)
716 if not ( isRead and hideReadArticles ):
717 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
718 #title = self.feed.getTitle(id)
719 title = self.fix_title(self.feed.getTitle(id))
721 #if self.feed.isEntryRead(id):
723 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
725 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
727 self.feedItems.append((markup, id))
730 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
732 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
733 self.feedItems.append((markup, ""))
735 self.add(self.pannableFeed)
739 self.pannableFeed.destroy()
740 #self.remove(self.pannableFeed)
742 def on_feedList_row_activated(self, treeview, path): #, column):
743 selection = self.feedList.get_selection()
744 selection.set_mode(gtk.SELECTION_SINGLE)
745 self.feedList.get_selection().select_path(path)
746 model = treeview.get_model()
747 iter = model.get_iter(path)
748 key = model.get_value(iter, FEED_COLUMN_KEY)
749 # Emulate legacy "button_clicked" call via treeview
750 gobject.idle_add(self.button_clicked, treeview, key)
753 def button_clicked(self, button, index, previous=False, next=False):
754 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
755 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
756 stack = hildon.WindowStack.get_default()
759 stack.pop_and_push(1, newDisp, tmp)
761 gobject.timeout_add(200, self.destroyArticle, tmp)
766 if type(self.disp).__name__ == "DisplayArticle":
767 gobject.timeout_add(200, self.destroyArticle, self.disp)
774 if self.key == "ArchivedArticles":
775 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
776 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
777 self.ids.append(self.disp.connect("article-next", self.nextArticle))
778 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
780 def buttonPurgeArticles(self, *widget):
782 self.feed.purgeReadArticles()
783 self.feed.saveUnread(CONFIGDIR)
784 self.feed.saveFeed(CONFIGDIR)
787 def destroyArticle(self, handle):
788 handle.destroyWindow()
790 def mark_item_read(self, key):
791 it = self.feedItems.get_iter_first()
792 while it is not None:
793 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
795 title = self.fix_title(self.feed.getTitle(key))
796 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
797 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
799 it = self.feedItems.iter_next(it)
801 def nextArticle(self, object, index):
802 self.mark_item_read(index)
803 id = self.feed.getNextId(index)
804 if self.config.getHideReadArticles():
807 isRead = self.feed.isEntryRead(id)
810 while isRead and id != index:
811 id = self.feed.getNextId(id)
814 isRead = self.feed.isEntryRead(id)
818 self.button_clicked(object, id, next=True)
820 def previousArticle(self, object, index):
821 self.mark_item_read(index)
822 id = self.feed.getPreviousId(index)
823 if self.config.getHideReadArticles():
826 isRead = self.feed.isEntryRead(id)
829 while isRead and id != index:
830 id = self.feed.getPreviousId(id)
833 isRead = self.feed.isEntryRead(id)
837 self.button_clicked(object, id, previous=True)
839 def onArticleClosed(self, object, index):
840 selection = self.feedList.get_selection()
841 selection.set_mode(gtk.SELECTION_NONE)
842 self.mark_item_read(index)
844 def onArticleDeleted(self, object, index):
846 self.feed.removeArticle(index)
847 self.feed.saveUnread(CONFIGDIR)
848 self.feed.saveFeed(CONFIGDIR)
851 def button_update_clicked(self, button):
852 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
853 if not type(self.downloadDialog).__name__=="DownloadBar":
854 self.pannableFeed.destroy()
855 self.vbox = gtk.VBox(False, 10)
856 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
857 self.downloadDialog.connect("download-done", self.onDownloadsDone)
858 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
862 def onDownloadsDone(self, *widget):
864 self.feed = self.listing.getFeed(self.key)
866 self.updateDbusHandler.ArticleCountUpdated()
868 def buttonReadAllClicked(self, button):
869 for index in self.feed.getIds():
870 self.feed.setEntryRead(index)
871 self.mark_item_read(index)
877 self.window = hildon.StackableWindow()
878 self.window.set_title(__appname__)
879 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
880 self.mainVbox = gtk.VBox(False,10)
882 self.introLabel = gtk.Label("Loading...")
885 self.mainVbox.pack_start(self.introLabel)
887 self.window.add(self.mainVbox)
888 self.window.show_all()
889 self.config = Config(self.window, CONFIGDIR+"config.ini")
890 gobject.idle_add(self.createWindow)
892 def createWindow(self):
893 self.app_lock = get_lock("app_lock")
894 if self.app_lock == None:
895 self.introLabel.set_label("Update in progress, please wait.")
896 gobject.timeout_add_seconds(3, self.createWindow)
898 self.listing = Listing(CONFIGDIR)
900 self.downloadDialog = False
902 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
903 self.orientation.set_mode(self.config.getOrientation())
905 print "Could not start rotation manager"
907 menu = hildon.AppMenu()
908 # Create a button and add it to the menu
909 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
910 button.set_label("Update feeds")
911 button.connect("clicked", self.button_update_clicked, "All")
914 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
915 button.set_label("Mark all as read")
916 button.connect("clicked", self.button_markAll)
919 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
920 button.set_label("Add new feed")
921 button.connect("clicked", lambda b: self.addFeed())
924 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925 button.set_label("Manage subscriptions")
926 button.connect("clicked", self.button_organize_clicked)
929 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930 button.set_label("Settings")
931 button.connect("clicked", self.button_preferences_clicked)
934 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
935 button.set_label("About")
936 button.connect("clicked", self.button_about_clicked)
939 self.window.set_app_menu(menu)
942 #self.feedWindow = hildon.StackableWindow()
943 #self.articleWindow = hildon.StackableWindow()
944 self.introLabel.destroy()
945 self.pannableListing = hildon.PannableArea()
946 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
947 self.feedList = gtk.TreeView(self.feedItems)
948 self.feedList.connect('row-activated', self.on_feedList_row_activated)
949 self.pannableListing.add(self.feedList)
951 icon_renderer = gtk.CellRendererPixbuf()
952 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
953 icon_column = gtk.TreeViewColumn('', icon_renderer, \
955 self.feedList.append_column(icon_column)
957 markup_renderer = gtk.CellRendererText()
958 markup_column = gtk.TreeViewColumn('', markup_renderer, \
959 markup=COLUMN_MARKUP)
960 self.feedList.append_column(markup_column)
961 self.mainVbox.pack_start(self.pannableListing)
962 self.mainVbox.show_all()
964 self.displayListing()
965 self.autoupdate = False
966 self.checkAutoUpdate()
967 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
968 gobject.idle_add(self.enableDbus)
970 def enableDbus(self):
971 self.dbusHandler = ServerObject(self)
972 self.updateDbusHandler = UpdateServerObject(self)
974 def button_markAll(self, button):
975 for key in self.listing.getListOfFeeds():
976 feed = self.listing.getFeed(key)
977 for id in feed.getIds():
978 feed.setEntryRead(id)
979 feed.saveUnread(CONFIGDIR)
980 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
981 self.displayListing()
983 def button_about_clicked(self, button):
984 HeAboutDialog.present(self.window, \
994 def button_export_clicked(self, button):
995 opml = ExportOpmlData(self.window, self.listing)
997 def button_import_clicked(self, button):
998 opml = GetOpmlData(self.window)
999 feeds = opml.getData()
1000 for (title, url) in feeds:
1001 self.listing.addFeed(title, url)
1002 self.displayListing()
1004 def addFeed(self, urlIn="http://"):
1005 wizard = AddWidgetWizard(self.window, urlIn)
1008 (title, url) = wizard.getData()
1009 if (not title == '') and (not url == ''):
1010 self.listing.addFeed(title, url)
1012 self.displayListing()
1014 def button_organize_clicked(self, button):
1015 def after_closing():
1016 self.listing.saveConfig()
1017 self.displayListing()
1018 SortList(self.window, self.listing, self, after_closing)
1020 def button_update_clicked(self, button, key):
1021 if not type(self.downloadDialog).__name__=="DownloadBar":
1022 self.updateDbusHandler.UpdateStarted()
1023 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1024 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1025 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1026 self.mainVbox.show_all()
1027 #self.displayListing()
1029 def onDownloadsDone(self, *widget):
1030 self.downloadDialog.destroy()
1031 self.downloadDialog = False
1032 self.displayListing()
1033 self.updateDbusHandler.UpdateFinished()
1034 self.updateDbusHandler.ArticleCountUpdated()
1036 def button_preferences_clicked(self, button):
1037 dialog = self.config.createDialog()
1038 dialog.connect("destroy", self.prefsClosed)
1040 def show_confirmation_note(self, parent, title):
1041 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1043 retcode = gtk.Dialog.run(note)
1046 if retcode == gtk.RESPONSE_OK:
1051 def displayListing(self):
1052 icon_theme = gtk.icon_theme_get_default()
1053 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1054 gtk.ICON_LOOKUP_USE_BUILTIN)
1056 self.feedItems.clear()
1057 for key in self.listing.getListOfFeeds():
1058 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1059 if unreadItems > 0 or not self.config.getHideReadFeeds():
1060 title = self.listing.getFeedTitle(key)
1061 updateTime = self.listing.getFeedUpdateTime(key)
1063 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1066 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1068 markup = FEED_TEMPLATE % (title, subtitle)
1071 icon_filename = self.listing.getFavicon(key)
1072 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1073 LIST_ICON_SIZE, LIST_ICON_SIZE)
1075 pixbuf = default_pixbuf
1077 self.feedItems.append((pixbuf, markup, key))
1079 def on_feedList_row_activated(self, treeview, path, column):
1080 model = treeview.get_model()
1081 iter = model.get_iter(path)
1082 key = model.get_value(iter, COLUMN_KEY)
1085 def openFeed(self, key):
1089 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1090 self.feed_lock = get_lock(key)
1091 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1092 self.listing.getFeedTitle(key), key, \
1093 self.config, self.updateDbusHandler)
1094 self.disp.connect("feed-closed", self.onFeedClosed)
1097 def onFeedClosed(self, object, key):
1098 #self.listing.saveConfig()
1100 gobject.idle_add(self.onFeedClosedTimeout)
1101 self.displayListing()
1102 #self.updateDbusHandler.ArticleCountUpdated()
1104 def onFeedClosedTimeout(self):
1105 self.listing.saveConfig()
1107 self.updateDbusHandler.ArticleCountUpdated()
1110 self.window.connect("destroy", gtk.main_quit)
1112 self.listing.saveConfig()
1115 def prefsClosed(self, *widget):
1117 self.orientation.set_mode(self.config.getOrientation())
1120 self.displayListing()
1121 self.checkAutoUpdate()
1123 def checkAutoUpdate(self, *widget):
1124 interval = int(self.config.getUpdateInterval()*3600000)
1125 if self.config.isAutoUpdateEnabled():
1126 if self.autoupdate == False:
1127 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1128 self.autoupdate = interval
1129 elif not self.autoupdate == interval:
1130 # If auto-update is enabled, but not at the right frequency
1131 gobject.source_remove(self.autoupdateId)
1132 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1133 self.autoupdate = interval
1135 if not self.autoupdate == False:
1136 gobject.source_remove(self.autoupdateId)
1137 self.autoupdate = False
1139 def automaticUpdate(self, *widget):
1140 # Need to check for internet connection
1141 # If no internet connection, try again in 10 minutes:
1142 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1143 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1144 #from time import localtime, strftime
1145 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1147 self.button_update_clicked(None, None)
1150 def stopUpdate(self):
1151 # Not implemented in the app (see update_feeds.py)
1153 self.downloadDialog.listOfKeys = []
1157 def getStatus(self):
1159 for key in self.listing.getListOfFeeds():
1160 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1161 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1163 status = "No unread items"
1166 if __name__ == "__main__":
1167 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1168 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1169 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1170 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1171 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1172 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1173 gobject.threads_init()
1174 if not isdir(CONFIGDIR):
1178 print "Error: Can't create configuration directory"
1179 from sys import exit