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 = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
90 # Build the markup template for the Maemo 5 text style
91 head_font = style.get_font_desc('SystemFont')
92 sub_font = style.get_font_desc('SmallSystemFont')
94 head_color = style.get_color('ButtonTextColor')
95 sub_color = style.get_color('DefaultTextColor')
96 active_color = style.get_color('ActiveTextColor')
98 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
99 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
101 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
102 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
104 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
105 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
107 entry_active_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), active_color.to_string())
108 entry_active_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), active_color.to_string())
110 FEED_TEMPLATE = '\n'.join((head, normal_sub))
111 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
113 ENTRY_TEMPLATE = entry_head
114 ENTRY_TEMPLATE_UNREAD = entry_active_head
117 # Removes HTML or XML character references and entities from a text string.
119 # @param text The HTML (or XML) source text.
120 # @return The plain text, as a Unicode string, if necessary.
121 # http://effbot.org/zone/re-sub.htm#unescape-html
126 # character reference
128 if text[:3] == "&#x":
129 return unichr(int(text[3:-1], 16))
131 return unichr(int(text[2:-1]))
137 text = unichr(name2codepoint[text[1:-1]])
140 return text # leave as is
141 return sub("&#?\w+;", fixup, text)
144 class AddWidgetWizard(hildon.WizardDialog):
146 def __init__(self, parent, urlIn, titleIn=None):
148 self.notebook = gtk.Notebook()
150 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
151 self.nameEntry.set_placeholder("Enter Feed Name")
152 vbox = gtk.VBox(False,10)
153 label = gtk.Label("Enter Feed Name:")
154 vbox.pack_start(label)
155 vbox.pack_start(self.nameEntry)
156 if not titleIn == None:
157 self.nameEntry.set_text(titleIn)
158 self.notebook.append_page(vbox, None)
160 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
161 self.urlEntry.set_placeholder("Enter a URL")
162 self.urlEntry.set_text(urlIn)
163 self.urlEntry.select_region(0,-1)
165 vbox = gtk.VBox(False,10)
166 label = gtk.Label("Enter Feed URL:")
167 vbox.pack_start(label)
168 vbox.pack_start(self.urlEntry)
169 self.notebook.append_page(vbox, None)
171 labelEnd = gtk.Label("Success")
173 self.notebook.append_page(labelEnd, None)
175 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
177 # Set a handler for "switch-page" signal
178 #self.notebook.connect("switch_page", self.on_page_switch, self)
180 # Set a function to decide if user can go to next page
181 self.set_forward_page_func(self.some_page_func)
186 return (self.nameEntry.get_text(), self.urlEntry.get_text())
188 def on_page_switch(self, notebook, page, num, dialog):
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 self.listing.removeFeed(key)
420 def buttonEdit(self, button):
421 key = self.getSelectedItem()
423 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
426 (title, url) = wizard.getData()
427 if (not title == '') and (not url == ''):
428 self.listing.editFeed(key, title, url)
432 def buttonDone(self, *args):
435 def buttonAdd(self, button, urlIn="http://"):
436 wizard = AddWidgetWizard(self, urlIn)
439 (title, url) = wizard.getData()
440 if (not title == '') and (not url == ''):
441 self.listing.addFeed(title, url)
446 class DisplayArticle(hildon.StackableWindow):
447 def __init__(self, feed, id, key, config, listing):
448 hildon.StackableWindow.__init__(self)
449 #self.imageDownloader = ImageDownloader()
454 #self.set_title(feed.getTitle(id))
455 self.set_title(self.listing.getFeedTitle(key))
457 self.set_for_removal = False
459 # Init the article display
460 #if self.config.getWebkitSupport():
461 self.view = WebView()
462 #self.view.set_editable(False)
465 # self.view = gtkhtml2.View()
466 # self.document = gtkhtml2.Document()
467 # self.view.set_document(self.document)
468 # self.document.connect("link_clicked", self._signal_link_clicked)
469 self.pannable_article = hildon.PannableArea()
470 self.pannable_article.add(self.view)
471 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
472 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
474 #if self.config.getWebkitSupport():
475 contentLink = self.feed.getContentLink(self.id)
476 self.feed.setEntryRead(self.id)
477 #if key=="ArchivedArticles":
478 if contentLink.startswith("/home/user/"):
479 self.view.open("file://" + contentLink)
481 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
482 self.view.connect("motion-notify-event", lambda w,ev: True)
483 self.view.connect('load-started', self.load_started)
484 self.view.connect('load-finished', self.load_finished)
487 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
488 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
490 # if not key == "ArchivedArticles":
491 # Do not download images if the feed is "Archived Articles"
492 # self.document.connect("request-url", self._signal_request_url)
494 # self.document.clear()
495 # self.document.open_stream("text/html")
496 # self.document.write_stream(self.text)
497 # self.document.close_stream()
499 menu = hildon.AppMenu()
500 # Create a button and add it to the menu
501 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
502 button.set_label("Allow horizontal scrolling")
503 button.connect("clicked", self.horiz_scrolling_button)
506 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
507 button.set_label("Open in browser")
508 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
511 if key == "ArchivedArticles":
512 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
513 button.set_label("Remove from archived articles")
514 button.connect("clicked", self.remove_archive_button)
516 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
517 button.set_label("Add to archived articles")
518 button.connect("clicked", self.archive_button)
521 self.set_app_menu(menu)
524 #self.event_box = gtk.EventBox()
525 #self.event_box.add(self.pannable_article)
526 self.add(self.pannable_article)
529 self.pannable_article.show_all()
531 self.destroyId = self.connect("destroy", self.destroyWindow)
533 self.view.connect("button_press_event", self.button_pressed)
534 self.gestureId = self.view.connect("button_release_event", self.button_released)
535 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
537 def load_started(self, *widget):
538 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
540 def load_finished(self, *widget):
541 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
543 def button_pressed(self, window, event):
544 #print event.x, event.y
545 self.coords = (event.x, event.y)
547 def button_released(self, window, event):
548 x = self.coords[0] - event.x
549 y = self.coords[1] - event.y
551 if (2*abs(y) < abs(x)):
553 self.emit("article-previous", self.id)
555 self.emit("article-next", self.id)
559 #def gesture(self, widget, direction, startx, starty):
560 # if (direction == 3):
561 # self.emit("article-next", self.index)
562 # if (direction == 2):
563 # self.emit("article-previous", self.index)
564 #print startx, starty
565 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
567 def destroyWindow(self, *args):
568 self.disconnect(self.destroyId)
569 if self.set_for_removal:
570 self.emit("article-deleted", self.id)
572 self.emit("article-closed", self.id)
573 #self.imageDownloader.stopAll()
576 def horiz_scrolling_button(self, *widget):
577 self.pannable_article.disconnect(self.gestureId)
578 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
580 def archive_button(self, *widget):
581 # Call the listing.addArchivedArticle
582 self.listing.addArchivedArticle(self.key, self.id)
584 def remove_archive_button(self, *widget):
585 self.set_for_removal = True
587 #def reloadArticle(self, *widget):
588 # if threading.activeCount() > 1:
589 # Image thread are still running, come back in a bit
592 # for (stream, imageThread) in self.images:
594 # stream.write(imageThread.data)
599 def _signal_link_clicked(self, object, link):
601 bus = dbus.SessionBus()
602 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
603 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
604 iface.open_new_window(link)
606 #def _signal_request_url(self, object, url, stream):
608 # self.imageDownloader.queueImage(url, stream)
609 #imageThread = GetImage(url)
611 #self.images.append((stream, imageThread))
614 class DisplayFeed(hildon.StackableWindow):
615 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
616 hildon.StackableWindow.__init__(self)
617 self.listing = listing
619 self.feedTitle = title
620 self.set_title(title)
623 self.updateDbusHandler = updateDbusHandler
625 self.downloadDialog = False
627 #self.listing.setCurrentlyDisplayedFeed(self.key)
631 menu = hildon.AppMenu()
632 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
633 button.set_label("Update feed")
634 button.connect("clicked", self.button_update_clicked)
637 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
638 button.set_label("Mark all as read")
639 button.connect("clicked", self.buttonReadAllClicked)
642 if key=="ArchivedArticles":
643 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
644 button.set_label("Delete read articles")
645 button.connect("clicked", self.buttonPurgeArticles)
648 self.set_app_menu(menu)
653 self.connect('configure-event', self.on_configure_event)
654 self.connect("destroy", self.destroyWindow)
656 def on_configure_event(self, window, event):
657 if getattr(self, 'markup_renderer', None) is None:
660 # Fix up the column width for wrapping the text when the window is
661 # resized (i.e. orientation changed)
662 self.markup_renderer.set_property('wrap-width', event.width-10)
664 def destroyWindow(self, *args):
665 #self.feed.saveUnread(CONFIGDIR)
666 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
667 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
668 self.emit("feed-closed", self.key)
670 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
671 #self.listing.closeCurrentlyDisplayedFeed()
673 def fix_title(self, title):
674 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
676 def displayFeed(self):
677 self.pannableFeed = hildon.PannableArea()
679 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
681 self.feedItems = gtk.ListStore(str, str)
682 #self.feedList = gtk.TreeView(self.feedItems)
683 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
684 selection = self.feedList.get_selection()
685 selection.set_mode(gtk.SELECTION_NONE)
686 #selection.connect("changed", lambda w: True)
688 self.feedList.set_model(self.feedItems)
689 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
692 self.feedList.set_hover_selection(False)
693 #self.feedList.set_property('enable-grid-lines', True)
694 #self.feedList.set_property('hildon-mode', 1)
695 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
697 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
699 vbox= gtk.VBox(False, 10)
700 vbox.pack_start(self.feedList)
702 self.pannableFeed.add_with_viewport(vbox)
704 self.markup_renderer = gtk.CellRendererText()
705 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
706 self.markup_renderer.set_property('wrap-width', 780)
707 self.markup_renderer.set_property('ypad', 5)
708 self.markup_renderer.set_property('xpad', 5)
709 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
710 markup=FEED_COLUMN_MARKUP)
711 self.feedList.append_column(markup_column)
713 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
714 hideReadArticles = self.config.getHideReadArticles()
716 for id in self.feed.getIds():
719 isRead = self.feed.isEntryRead(id)
722 if not ( isRead and hideReadArticles ):
723 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
724 #title = self.feed.getTitle(id)
725 title = self.fix_title(self.feed.getTitle(id))
727 #if self.feed.isEntryRead(id):
729 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
731 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
733 self.feedItems.append((markup, id))
736 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
738 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
739 self.feedItems.append((markup, ""))
741 self.add(self.pannableFeed)
745 self.pannableFeed.destroy()
746 #self.remove(self.pannableFeed)
748 def on_feedList_row_activated(self, treeview, path): #, column):
749 selection = self.feedList.get_selection()
750 selection.set_mode(gtk.SELECTION_SINGLE)
751 self.feedList.get_selection().select_path(path)
752 model = treeview.get_model()
753 iter = model.get_iter(path)
754 key = model.get_value(iter, FEED_COLUMN_KEY)
755 # Emulate legacy "button_clicked" call via treeview
756 gobject.idle_add(self.button_clicked, treeview, key)
759 def button_clicked(self, button, index, previous=False, next=False):
760 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
761 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
762 stack = hildon.WindowStack.get_default()
765 stack.pop_and_push(1, newDisp, tmp)
767 gobject.timeout_add(200, self.destroyArticle, tmp)
772 if type(self.disp).__name__ == "DisplayArticle":
773 gobject.timeout_add(200, self.destroyArticle, self.disp)
780 if self.key == "ArchivedArticles":
781 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
782 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
783 self.ids.append(self.disp.connect("article-next", self.nextArticle))
784 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
786 def buttonPurgeArticles(self, *widget):
788 self.feed.purgeReadArticles()
789 self.feed.saveUnread(CONFIGDIR)
790 self.feed.saveFeed(CONFIGDIR)
793 def destroyArticle(self, handle):
794 handle.destroyWindow()
796 def mark_item_read(self, key):
797 it = self.feedItems.get_iter_first()
798 while it is not None:
799 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
801 title = self.fix_title(self.feed.getTitle(key))
802 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
803 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
805 it = self.feedItems.iter_next(it)
807 def nextArticle(self, object, index):
808 self.mark_item_read(index)
809 id = self.feed.getNextId(index)
810 if self.config.getHideReadArticles():
813 isRead = self.feed.isEntryRead(id)
816 while isRead and id != index:
817 id = self.feed.getNextId(id)
820 isRead = self.feed.isEntryRead(id)
824 self.button_clicked(object, id, next=True)
826 def previousArticle(self, object, index):
827 self.mark_item_read(index)
828 id = self.feed.getPreviousId(index)
829 if self.config.getHideReadArticles():
832 isRead = self.feed.isEntryRead(id)
835 while isRead and id != index:
836 id = self.feed.getPreviousId(id)
839 isRead = self.feed.isEntryRead(id)
843 self.button_clicked(object, id, previous=True)
845 def onArticleClosed(self, object, index):
846 selection = self.feedList.get_selection()
847 selection.set_mode(gtk.SELECTION_NONE)
848 self.mark_item_read(index)
850 def onArticleDeleted(self, object, index):
852 self.feed.removeArticle(index)
853 self.feed.saveUnread(CONFIGDIR)
854 self.feed.saveFeed(CONFIGDIR)
857 def button_update_clicked(self, button):
858 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
859 if not type(self.downloadDialog).__name__=="DownloadBar":
860 self.pannableFeed.destroy()
861 self.vbox = gtk.VBox(False, 10)
862 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
863 self.downloadDialog.connect("download-done", self.onDownloadsDone)
864 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
868 def onDownloadsDone(self, *widget):
870 self.feed = self.listing.getFeed(self.key)
872 self.updateDbusHandler.ArticleCountUpdated()
874 def buttonReadAllClicked(self, button):
875 for index in self.feed.getIds():
876 self.feed.setEntryRead(index)
877 self.mark_item_read(index)
883 self.window = hildon.StackableWindow()
884 self.window.set_title(__appname__)
885 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
886 self.mainVbox = gtk.VBox(False,10)
888 self.introLabel = gtk.Label("Loading...")
891 self.mainVbox.pack_start(self.introLabel)
893 self.window.add(self.mainVbox)
894 self.window.show_all()
895 self.config = Config(self.window, CONFIGDIR+"config.ini")
896 gobject.idle_add(self.createWindow)
898 def createWindow(self):
899 self.app_lock = get_lock("app_lock")
900 if self.app_lock == None:
901 self.introLabel.set_label("Update in progress, please wait.")
902 gobject.timeout_add_seconds(3, self.createWindow)
904 self.listing = Listing(CONFIGDIR)
906 self.downloadDialog = False
908 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
909 self.orientation.set_mode(self.config.getOrientation())
911 print "Could not start rotation manager"
913 menu = hildon.AppMenu()
914 # Create a button and add it to the menu
915 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
916 button.set_label("Update feeds")
917 button.connect("clicked", self.button_update_clicked, "All")
920 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
921 button.set_label("Mark all as read")
922 button.connect("clicked", self.button_markAll)
925 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
926 button.set_label("Manage subscriptions")
927 button.connect("clicked", self.button_organize_clicked)
930 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
931 button.set_label("Settings")
932 button.connect("clicked", self.button_preferences_clicked)
935 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
936 button.set_label("About")
937 button.connect("clicked", self.button_about_clicked)
940 self.window.set_app_menu(menu)
943 #self.feedWindow = hildon.StackableWindow()
944 #self.articleWindow = hildon.StackableWindow()
945 self.introLabel.destroy()
946 self.pannableListing = hildon.PannableArea()
947 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
948 self.feedList = gtk.TreeView(self.feedItems)
949 self.feedList.connect('row-activated', self.on_feedList_row_activated)
950 self.pannableListing.add(self.feedList)
952 icon_renderer = gtk.CellRendererPixbuf()
953 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
954 icon_column = gtk.TreeViewColumn('', icon_renderer, \
956 self.feedList.append_column(icon_column)
958 markup_renderer = gtk.CellRendererText()
959 markup_column = gtk.TreeViewColumn('', markup_renderer, \
960 markup=COLUMN_MARKUP)
961 self.feedList.append_column(markup_column)
962 self.mainVbox.pack_start(self.pannableListing)
963 self.mainVbox.show_all()
965 self.displayListing()
966 self.autoupdate = False
967 self.checkAutoUpdate()
968 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
969 gobject.idle_add(self.enableDbus)
971 def enableDbus(self):
972 self.dbusHandler = ServerObject(self)
973 self.updateDbusHandler = UpdateServerObject(self)
975 def button_markAll(self, button):
976 for key in self.listing.getListOfFeeds():
977 feed = self.listing.getFeed(key)
978 for id in feed.getIds():
979 feed.setEntryRead(id)
980 feed.saveUnread(CONFIGDIR)
981 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
982 self.displayListing()
984 def button_about_clicked(self, button):
985 HeAboutDialog.present(self.window, \
995 def button_export_clicked(self, button):
996 opml = ExportOpmlData(self.window, self.listing)
998 def button_import_clicked(self, button):
999 opml = GetOpmlData(self.window)
1000 feeds = opml.getData()
1001 for (title, url) in feeds:
1002 self.listing.addFeed(title, url)
1003 self.displayListing()
1005 def addFeed(self, urlIn="http://"):
1006 wizard = AddWidgetWizard(self.window, urlIn)
1009 (title, url) = wizard.getData()
1010 if (not title == '') and (not url == ''):
1011 self.listing.addFeed(title, url)
1013 self.displayListing()
1015 def button_organize_clicked(self, button):
1016 def after_closing():
1017 self.listing.saveConfig()
1018 self.displayListing()
1019 SortList(self.window, self.listing, self, after_closing)
1021 def button_update_clicked(self, button, key):
1022 if not type(self.downloadDialog).__name__=="DownloadBar":
1023 self.updateDbusHandler.UpdateStarted()
1024 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1025 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1026 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1027 self.mainVbox.show_all()
1028 #self.displayListing()
1030 def onDownloadsDone(self, *widget):
1031 self.downloadDialog.destroy()
1032 self.downloadDialog = False
1033 self.displayListing()
1034 self.updateDbusHandler.UpdateFinished()
1035 self.updateDbusHandler.ArticleCountUpdated()
1037 def button_preferences_clicked(self, button):
1038 dialog = self.config.createDialog()
1039 dialog.connect("destroy", self.prefsClosed)
1041 def show_confirmation_note(self, parent, title):
1042 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1044 retcode = gtk.Dialog.run(note)
1047 if retcode == gtk.RESPONSE_OK:
1052 def displayListing(self):
1053 icon_theme = gtk.icon_theme_get_default()
1054 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1055 gtk.ICON_LOOKUP_USE_BUILTIN)
1057 self.feedItems.clear()
1060 for key in self.listing.getListOfFeeds():
1061 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1062 if unreadItems > 0 or not self.config.getHideReadFeeds():
1064 title = self.listing.getFeedTitle(key)
1065 updateTime = self.listing.getFeedUpdateTime(key)
1066 updateStamp = self.listing.getFeedUpdateStamp(key)
1067 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1068 feedInfo[key] = [count, unreadItems, updateStamp, title, subtitle, updateTime];
1070 order = self.config.getFeedSortOrder();
1071 if order == "Most unread":
1072 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
1073 elif order == "Least unread":
1074 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
1075 elif order == "Most recent":
1076 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
1077 elif order == "Least recent":
1078 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
1079 else: # order == "Manual" or invalid value...
1080 keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
1082 for key in keyorder:
1083 unreadItems = feedInfo[key][1]
1084 title = xml.sax.saxutils.escape(feedInfo[key][3])
1085 subtitle = feedInfo[key][4]
1086 updateTime = feedInfo[key][5]
1088 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1090 markup = FEED_TEMPLATE % (title, subtitle)
1093 icon_filename = self.listing.getFavicon(key)
1094 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1095 LIST_ICON_SIZE, LIST_ICON_SIZE)
1097 pixbuf = default_pixbuf
1099 self.feedItems.append((pixbuf, markup, key))
1101 def on_feedList_row_activated(self, treeview, path, column):
1102 model = treeview.get_model()
1103 iter = model.get_iter(path)
1104 key = model.get_value(iter, COLUMN_KEY)
1107 def openFeed(self, key):
1111 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1112 self.feed_lock = get_lock(key)
1113 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1114 self.listing.getFeedTitle(key), key, \
1115 self.config, self.updateDbusHandler)
1116 self.disp.connect("feed-closed", self.onFeedClosed)
1119 def onFeedClosed(self, object, key):
1120 #self.listing.saveConfig()
1122 gobject.idle_add(self.onFeedClosedTimeout)
1123 self.displayListing()
1124 #self.updateDbusHandler.ArticleCountUpdated()
1126 def onFeedClosedTimeout(self):
1127 self.listing.saveConfig()
1129 self.updateDbusHandler.ArticleCountUpdated()
1132 self.window.connect("destroy", gtk.main_quit)
1134 self.listing.saveConfig()
1137 def prefsClosed(self, *widget):
1139 self.orientation.set_mode(self.config.getOrientation())
1142 self.displayListing()
1143 self.checkAutoUpdate()
1145 def checkAutoUpdate(self, *widget):
1146 interval = int(self.config.getUpdateInterval()*3600000)
1147 if self.config.isAutoUpdateEnabled():
1148 if self.autoupdate == False:
1149 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1150 self.autoupdate = interval
1151 elif not self.autoupdate == interval:
1152 # If auto-update is enabled, but not at the right frequency
1153 gobject.source_remove(self.autoupdateId)
1154 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1155 self.autoupdate = interval
1157 if not self.autoupdate == False:
1158 gobject.source_remove(self.autoupdateId)
1159 self.autoupdate = False
1161 def automaticUpdate(self, *widget):
1162 # Need to check for internet connection
1163 # If no internet connection, try again in 10 minutes:
1164 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1165 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1166 #from time import localtime, strftime
1167 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1169 self.button_update_clicked(None, None)
1172 def stopUpdate(self):
1173 # Not implemented in the app (see update_feeds.py)
1175 self.downloadDialog.listOfKeys = []
1179 def getStatus(self):
1181 for key in self.listing.getListOfFeeds():
1182 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1183 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1185 status = "No unread items"
1188 if __name__ == "__main__":
1189 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1190 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1191 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1192 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1193 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1194 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1195 gobject.threads_init()
1196 if not isdir(CONFIGDIR):
1200 print "Error: Can't create configuration directory"
1201 from sys import exit