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 if contentLink.startswith("/home/user/"):
488 self.view.open("file://" + contentLink)
490 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
491 self.view.connect("motion-notify-event", lambda w,ev: True)
492 self.view.connect('load-started', self.load_started)
493 self.view.connect('load-finished', self.load_finished)
496 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
497 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
499 # if not key == "ArchivedArticles":
500 # Do not download images if the feed is "Archived Articles"
501 # self.document.connect("request-url", self._signal_request_url)
503 # self.document.clear()
504 # self.document.open_stream("text/html")
505 # self.document.write_stream(self.text)
506 # self.document.close_stream()
508 menu = hildon.AppMenu()
509 # Create a button and add it to the menu
510 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
511 button.set_label("Allow horizontal scrolling")
512 button.connect("clicked", self.horiz_scrolling_button)
515 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
516 button.set_label("Open in browser")
517 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
520 if key == "ArchivedArticles":
521 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
522 button.set_label("Remove from archived articles")
523 button.connect("clicked", self.remove_archive_button)
525 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
526 button.set_label("Add to archived articles")
527 button.connect("clicked", self.archive_button)
530 self.set_app_menu(menu)
533 #self.event_box = gtk.EventBox()
534 #self.event_box.add(self.pannable_article)
535 self.add(self.pannable_article)
538 self.pannable_article.show_all()
540 self.destroyId = self.connect("destroy", self.destroyWindow)
542 self.view.connect("button_press_event", self.button_pressed)
543 self.gestureId = self.view.connect("button_release_event", self.button_released)
544 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
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)
552 def button_pressed(self, window, event):
553 #print event.x, event.y
554 self.coords = (event.x, event.y)
556 def button_released(self, window, event):
557 x = self.coords[0] - event.x
558 y = self.coords[1] - event.y
560 if (2*abs(y) < abs(x)):
562 self.emit("article-previous", self.id)
564 self.emit("article-next", self.id)
568 #def gesture(self, widget, direction, startx, starty):
569 # if (direction == 3):
570 # self.emit("article-next", self.index)
571 # if (direction == 2):
572 # self.emit("article-previous", self.index)
573 #print startx, starty
574 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
576 def destroyWindow(self, *args):
577 self.disconnect(self.destroyId)
578 if self.set_for_removal:
579 self.emit("article-deleted", self.id)
581 self.emit("article-closed", self.id)
582 #self.imageDownloader.stopAll()
585 def horiz_scrolling_button(self, *widget):
586 self.pannable_article.disconnect(self.gestureId)
587 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
589 def archive_button(self, *widget):
590 # Call the listing.addArchivedArticle
591 self.listing.addArchivedArticle(self.key, self.id)
593 def remove_archive_button(self, *widget):
594 self.set_for_removal = True
596 #def reloadArticle(self, *widget):
597 # if threading.activeCount() > 1:
598 # Image thread are still running, come back in a bit
601 # for (stream, imageThread) in self.images:
603 # stream.write(imageThread.data)
608 def _signal_link_clicked(self, object, link):
610 bus = dbus.SessionBus()
611 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
612 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
613 iface.open_new_window(link)
615 #def _signal_request_url(self, object, url, stream):
617 # self.imageDownloader.queueImage(url, stream)
618 #imageThread = GetImage(url)
620 #self.images.append((stream, imageThread))
623 class DisplayFeed(hildon.StackableWindow):
624 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
625 hildon.StackableWindow.__init__(self)
626 self.listing = listing
628 self.feedTitle = title
629 self.set_title(title)
632 self.updateDbusHandler = updateDbusHandler
634 self.downloadDialog = False
636 #self.listing.setCurrentlyDisplayedFeed(self.key)
640 menu = hildon.AppMenu()
641 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
642 button.set_label("Update feed")
643 button.connect("clicked", self.button_update_clicked)
646 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
647 button.set_label("Mark all as read")
648 button.connect("clicked", self.buttonReadAllClicked)
651 if key=="ArchivedArticles":
652 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
653 button.set_label("Delete read articles")
654 button.connect("clicked", self.buttonPurgeArticles)
657 self.set_app_menu(menu)
662 self.connect('configure-event', self.on_configure_event)
663 self.connect("destroy", self.destroyWindow)
665 def on_configure_event(self, window, event):
666 if getattr(self, 'markup_renderer', None) is None:
669 # Fix up the column width for wrapping the text when the window is
670 # resized (i.e. orientation changed)
671 self.markup_renderer.set_property('wrap-width', event.width-20)
672 it = self.feedItems.get_iter_first()
673 while it is not None:
674 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
675 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
676 it = self.feedItems.iter_next(it)
678 def destroyWindow(self, *args):
679 #self.feed.saveUnread(CONFIGDIR)
680 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
681 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
682 self.emit("feed-closed", self.key)
684 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
685 #self.listing.closeCurrentlyDisplayedFeed()
687 def fix_title(self, title):
688 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
690 def displayFeed(self):
691 self.pannableFeed = hildon.PannableArea()
693 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
695 self.feedItems = gtk.ListStore(str, str)
696 #self.feedList = gtk.TreeView(self.feedItems)
697 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
698 selection = self.feedList.get_selection()
699 selection.set_mode(gtk.SELECTION_NONE)
700 #selection.connect("changed", lambda w: True)
702 self.feedList.set_model(self.feedItems)
703 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
706 self.feedList.set_hover_selection(False)
707 #self.feedList.set_property('enable-grid-lines', True)
708 #self.feedList.set_property('hildon-mode', 1)
709 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
711 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
713 vbox= gtk.VBox(False, 10)
714 vbox.pack_start(self.feedList)
716 self.pannableFeed.add_with_viewport(vbox)
718 self.markup_renderer = gtk.CellRendererText()
719 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
720 self.markup_renderer.set_property('background', "#333333")
721 (width, height) = self.get_size()
722 self.markup_renderer.set_property('wrap-width', width-20)
723 self.markup_renderer.set_property('ypad', 5)
724 self.markup_renderer.set_property('xpad', 5)
725 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
726 markup=FEED_COLUMN_MARKUP)
727 self.feedList.append_column(markup_column)
729 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
730 hideReadArticles = self.config.getHideReadArticles()
732 for id in self.feed.getIds():
735 isRead = self.feed.isEntryRead(id)
738 if not ( isRead and hideReadArticles ):
739 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
740 #title = self.feed.getTitle(id)
741 title = self.fix_title(self.feed.getTitle(id))
743 #if self.feed.isEntryRead(id):
745 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
747 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
749 self.feedItems.append((markup, id))
752 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
754 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
755 self.feedItems.append((markup, ""))
757 self.add(self.pannableFeed)
761 self.pannableFeed.destroy()
762 #self.remove(self.pannableFeed)
764 def on_feedList_row_activated(self, treeview, path): #, column):
765 selection = self.feedList.get_selection()
766 selection.set_mode(gtk.SELECTION_SINGLE)
767 self.feedList.get_selection().select_path(path)
768 model = treeview.get_model()
769 iter = model.get_iter(path)
770 key = model.get_value(iter, FEED_COLUMN_KEY)
771 # Emulate legacy "button_clicked" call via treeview
772 gobject.idle_add(self.button_clicked, treeview, key)
775 def button_clicked(self, button, index, previous=False, next=False):
776 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
777 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
778 stack = hildon.WindowStack.get_default()
781 stack.pop_and_push(1, newDisp, tmp)
783 gobject.timeout_add(200, self.destroyArticle, tmp)
788 if type(self.disp).__name__ == "DisplayArticle":
789 gobject.timeout_add(200, self.destroyArticle, self.disp)
796 if self.key == "ArchivedArticles":
797 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
798 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
799 self.ids.append(self.disp.connect("article-next", self.nextArticle))
800 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
802 def buttonPurgeArticles(self, *widget):
804 self.feed.purgeReadArticles()
805 self.feed.saveUnread(CONFIGDIR)
806 self.feed.saveFeed(CONFIGDIR)
809 def destroyArticle(self, handle):
810 handle.destroyWindow()
812 def mark_item_read(self, key):
813 it = self.feedItems.get_iter_first()
814 while it is not None:
815 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
817 title = self.fix_title(self.feed.getTitle(key))
818 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
819 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
821 it = self.feedItems.iter_next(it)
823 def nextArticle(self, object, index):
824 self.mark_item_read(index)
825 id = self.feed.getNextId(index)
826 if self.config.getHideReadArticles():
829 isRead = self.feed.isEntryRead(id)
832 while isRead and id != index:
833 id = self.feed.getNextId(id)
836 isRead = self.feed.isEntryRead(id)
840 self.button_clicked(object, id, next=True)
842 def previousArticle(self, object, index):
843 self.mark_item_read(index)
844 id = self.feed.getPreviousId(index)
845 if self.config.getHideReadArticles():
848 isRead = self.feed.isEntryRead(id)
851 while isRead and id != index:
852 id = self.feed.getPreviousId(id)
855 isRead = self.feed.isEntryRead(id)
859 self.button_clicked(object, id, previous=True)
861 def onArticleClosed(self, object, index):
862 selection = self.feedList.get_selection()
863 selection.set_mode(gtk.SELECTION_NONE)
864 self.mark_item_read(index)
866 def onArticleDeleted(self, object, index):
868 self.feed.removeArticle(index)
869 self.feed.saveUnread(CONFIGDIR)
870 self.feed.saveFeed(CONFIGDIR)
873 def button_update_clicked(self, button):
874 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
875 if not type(self.downloadDialog).__name__=="DownloadBar":
876 self.pannableFeed.destroy()
877 self.vbox = gtk.VBox(False, 10)
878 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
879 self.downloadDialog.connect("download-done", self.onDownloadsDone)
880 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
884 def onDownloadsDone(self, *widget):
886 self.feed = self.listing.getFeed(self.key)
888 self.updateDbusHandler.ArticleCountUpdated()
890 def buttonReadAllClicked(self, button):
891 for index in self.feed.getIds():
892 self.feed.setEntryRead(index)
893 self.mark_item_read(index)
899 self.window = hildon.StackableWindow()
900 self.window.set_title(__appname__)
901 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
902 self.mainVbox = gtk.VBox(False,10)
904 self.introLabel = gtk.Label("Loading...")
907 self.mainVbox.pack_start(self.introLabel)
909 self.window.add(self.mainVbox)
910 self.window.show_all()
911 self.config = Config(self.window, CONFIGDIR+"config.ini")
912 gobject.idle_add(self.createWindow)
914 def createWindow(self):
915 self.app_lock = get_lock("app_lock")
916 if self.app_lock == None:
917 self.introLabel.set_label("Update in progress, please wait.")
918 gobject.timeout_add_seconds(3, self.createWindow)
920 self.listing = Listing(CONFIGDIR)
922 self.downloadDialog = False
924 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
925 self.orientation.set_mode(self.config.getOrientation())
927 print "Could not start rotation manager"
929 menu = hildon.AppMenu()
930 # Create a button and add it to the menu
931 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
932 button.set_label("Update feeds")
933 button.connect("clicked", self.button_update_clicked, "All")
936 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
937 button.set_label("Mark all as read")
938 button.connect("clicked", self.button_markAll)
941 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
942 button.set_label("Add new feed")
943 button.connect("clicked", lambda b: self.addFeed())
946 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
947 button.set_label("Manage subscriptions")
948 button.connect("clicked", self.button_organize_clicked)
951 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
952 button.set_label("Settings")
953 button.connect("clicked", self.button_preferences_clicked)
956 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
957 button.set_label("About")
958 button.connect("clicked", self.button_about_clicked)
961 self.window.set_app_menu(menu)
964 #self.feedWindow = hildon.StackableWindow()
965 #self.articleWindow = hildon.StackableWindow()
966 self.introLabel.destroy()
967 self.pannableListing = hildon.PannableArea()
968 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
969 self.feedList = gtk.TreeView(self.feedItems)
970 self.feedList.connect('row-activated', self.on_feedList_row_activated)
971 self.pannableListing.add(self.feedList)
973 icon_renderer = gtk.CellRendererPixbuf()
974 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
975 icon_column = gtk.TreeViewColumn('', icon_renderer, \
977 self.feedList.append_column(icon_column)
979 markup_renderer = gtk.CellRendererText()
980 markup_column = gtk.TreeViewColumn('', markup_renderer, \
981 markup=COLUMN_MARKUP)
982 self.feedList.append_column(markup_column)
983 self.mainVbox.pack_start(self.pannableListing)
984 self.mainVbox.show_all()
986 self.displayListing()
987 self.autoupdate = False
988 self.checkAutoUpdate()
989 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
990 gobject.idle_add(self.enableDbus)
992 def enableDbus(self):
993 self.dbusHandler = ServerObject(self)
994 self.updateDbusHandler = UpdateServerObject(self)
996 def button_markAll(self, button):
997 for key in self.listing.getListOfFeeds():
998 feed = self.listing.getFeed(key)
999 for id in feed.getIds():
1000 feed.setEntryRead(id)
1001 feed.saveUnread(CONFIGDIR)
1002 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
1003 self.displayListing()
1005 def button_about_clicked(self, button):
1006 HeAboutDialog.present(self.window, \
1016 def button_export_clicked(self, button):
1017 opml = ExportOpmlData(self.window, self.listing)
1019 def button_import_clicked(self, button):
1020 opml = GetOpmlData(self.window)
1021 feeds = opml.getData()
1022 for (title, url) in feeds:
1023 self.listing.addFeed(title, url)
1024 self.displayListing()
1026 def addFeed(self, urlIn="http://"):
1027 wizard = AddWidgetWizard(self.window, urlIn)
1030 (title, url) = wizard.getData()
1031 if (not title == '') and (not url == ''):
1032 self.listing.addFeed(title, url)
1034 self.displayListing()
1036 def button_organize_clicked(self, button):
1037 def after_closing():
1038 self.listing.saveConfig()
1039 self.displayListing()
1040 SortList(self.window, self.listing, self, after_closing)
1042 def button_update_clicked(self, button, key):
1043 if not type(self.downloadDialog).__name__=="DownloadBar":
1044 self.updateDbusHandler.UpdateStarted()
1045 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1046 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1047 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1048 self.mainVbox.show_all()
1049 #self.displayListing()
1051 def onDownloadsDone(self, *widget):
1052 self.downloadDialog.destroy()
1053 self.downloadDialog = False
1054 self.displayListing()
1055 self.updateDbusHandler.UpdateFinished()
1056 self.updateDbusHandler.ArticleCountUpdated()
1058 def button_preferences_clicked(self, button):
1059 dialog = self.config.createDialog()
1060 dialog.connect("destroy", self.prefsClosed)
1062 def show_confirmation_note(self, parent, title):
1063 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1065 retcode = gtk.Dialog.run(note)
1068 if retcode == gtk.RESPONSE_OK:
1073 def displayListing(self):
1074 icon_theme = gtk.icon_theme_get_default()
1075 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1076 gtk.ICON_LOOKUP_USE_BUILTIN)
1078 self.feedItems.clear()
1079 for key in self.listing.getListOfFeeds():
1080 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1081 if unreadItems > 0 or not self.config.getHideReadFeeds():
1082 title = self.listing.getFeedTitle(key)
1083 updateTime = self.listing.getFeedUpdateTime(key)
1085 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
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