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 = '<span font_desc="%s %%s" foreground="%s">%%s</span>'
88 # Build the markup template for the Maemo 5 text style
89 head_font = style.get_font_desc('SystemFont')
90 sub_font = style.get_font_desc('SmallSystemFont')
92 head_color = style.get_color('ButtonTextColor')
93 sub_color = style.get_color('DefaultTextColor')
94 active_color = style.get_color('ActiveTextColor')
96 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
97 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
99 entry_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), head_color.to_string())
100 entry_normal_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), sub_color.to_string())
102 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
103 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
105 entry_active_head = MARKUP_TEMPLATE_ENTRY % (head_font.get_family(), active_color.to_string())
106 entry_active_sub = MARKUP_TEMPLATE_ENTRY % (sub_font.get_family(), active_color.to_string())
108 FEED_TEMPLATE = '\n'.join((head, normal_sub))
109 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
111 ENTRY_TEMPLATE = entry_head
112 ENTRY_TEMPLATE_UNREAD = entry_active_head
115 # Removes HTML or XML character references and entities from a text string.
117 # @param text The HTML (or XML) source text.
118 # @return The plain text, as a Unicode string, if necessary.
119 # http://effbot.org/zone/re-sub.htm#unescape-html
124 # character reference
126 if text[:3] == "&#x":
127 return unichr(int(text[3:-1], 16))
129 return unichr(int(text[2:-1]))
135 text = unichr(name2codepoint[text[1:-1]])
138 return text # leave as is
139 return sub("&#?\w+;", fixup, text)
142 class AddWidgetWizard(hildon.WizardDialog):
144 def __init__(self, parent, urlIn, titleIn=None):
146 self.notebook = gtk.Notebook()
148 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
149 self.nameEntry.set_placeholder("Enter Feed Name")
150 vbox = gtk.VBox(False,10)
151 label = gtk.Label("Enter Feed Name:")
152 vbox.pack_start(label)
153 vbox.pack_start(self.nameEntry)
154 if not titleIn == None:
155 self.nameEntry.set_text(titleIn)
156 self.notebook.append_page(vbox, None)
158 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
159 self.urlEntry.set_placeholder("Enter a URL")
160 self.urlEntry.set_text(urlIn)
161 self.urlEntry.select_region(0,-1)
163 vbox = gtk.VBox(False,10)
164 label = gtk.Label("Enter Feed URL:")
165 vbox.pack_start(label)
166 vbox.pack_start(self.urlEntry)
167 self.notebook.append_page(vbox, None)
169 labelEnd = gtk.Label("Success")
171 self.notebook.append_page(labelEnd, None)
173 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
175 # Set a handler for "switch-page" signal
176 #self.notebook.connect("switch_page", self.on_page_switch, self)
178 # Set a function to decide if user can go to next page
179 self.set_forward_page_func(self.some_page_func)
184 return (self.nameEntry.get_text(), self.urlEntry.get_text())
186 def on_page_switch(self, notebook, page, num, dialog):
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 self.listing.removeFeed(key)
418 def buttonEdit(self, button):
419 key = self.getSelectedItem()
421 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
424 (title, url) = wizard.getData()
425 if (not title == '') and (not url == ''):
426 self.listing.editFeed(key, title, url)
430 def buttonDone(self, *args):
433 def buttonAdd(self, button, urlIn="http://"):
434 wizard = AddWidgetWizard(self, urlIn)
437 (title, url) = wizard.getData()
438 if (not title == '') and (not url == ''):
439 self.listing.addFeed(title, url)
444 class DisplayArticle(hildon.StackableWindow):
445 def __init__(self, feed, id, key, config, listing):
446 hildon.StackableWindow.__init__(self)
447 #self.imageDownloader = ImageDownloader()
452 #self.set_title(feed.getTitle(id))
453 self.set_title(self.listing.getFeedTitle(key))
455 self.set_for_removal = False
457 # Init the article display
458 #if self.config.getWebkitSupport():
459 self.view = WebView()
460 #self.view.set_editable(False)
463 # self.view = gtkhtml2.View()
464 # self.document = gtkhtml2.Document()
465 # self.view.set_document(self.document)
466 # self.document.connect("link_clicked", self._signal_link_clicked)
467 self.pannable_article = hildon.PannableArea()
468 self.pannable_article.add(self.view)
469 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
470 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
472 #if self.config.getWebkitSupport():
473 contentLink = self.feed.getContentLink(self.id)
474 self.feed.setEntryRead(self.id)
475 #if key=="ArchivedArticles":
476 if contentLink.startswith("/home/user/"):
477 self.view.open("file://" + contentLink)
479 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
480 self.view.connect("motion-notify-event", lambda w,ev: True)
481 self.view.connect('load-started', self.load_started)
482 self.view.connect('load-finished', self.load_finished)
485 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
486 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
488 # if not key == "ArchivedArticles":
489 # Do not download images if the feed is "Archived Articles"
490 # self.document.connect("request-url", self._signal_request_url)
492 # self.document.clear()
493 # self.document.open_stream("text/html")
494 # self.document.write_stream(self.text)
495 # self.document.close_stream()
497 menu = hildon.AppMenu()
498 # Create a button and add it to the menu
499 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
500 button.set_label("Allow horizontal scrolling")
501 button.connect("clicked", self.horiz_scrolling_button)
504 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
505 button.set_label("Open in browser")
506 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
509 if key == "ArchivedArticles":
510 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
511 button.set_label("Remove from archived articles")
512 button.connect("clicked", self.remove_archive_button)
514 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
515 button.set_label("Add to archived articles")
516 button.connect("clicked", self.archive_button)
519 self.set_app_menu(menu)
522 #self.event_box = gtk.EventBox()
523 #self.event_box.add(self.pannable_article)
524 self.add(self.pannable_article)
527 self.pannable_article.show_all()
529 self.destroyId = self.connect("destroy", self.destroyWindow)
531 self.view.connect("button_press_event", self.button_pressed)
532 self.gestureId = self.view.connect("button_release_event", self.button_released)
533 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
535 def load_started(self, *widget):
536 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
538 def load_finished(self, *widget):
539 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
541 def button_pressed(self, window, event):
542 #print event.x, event.y
543 self.coords = (event.x, event.y)
545 def button_released(self, window, event):
546 x = self.coords[0] - event.x
547 y = self.coords[1] - event.y
549 if (2*abs(y) < abs(x)):
551 self.emit("article-previous", self.id)
553 self.emit("article-next", self.id)
557 #def gesture(self, widget, direction, startx, starty):
558 # if (direction == 3):
559 # self.emit("article-next", self.index)
560 # if (direction == 2):
561 # self.emit("article-previous", self.index)
562 #print startx, starty
563 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
565 def destroyWindow(self, *args):
566 self.disconnect(self.destroyId)
567 if self.set_for_removal:
568 self.emit("article-deleted", self.id)
570 self.emit("article-closed", self.id)
571 #self.imageDownloader.stopAll()
574 def horiz_scrolling_button(self, *widget):
575 self.pannable_article.disconnect(self.gestureId)
576 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
578 def archive_button(self, *widget):
579 # Call the listing.addArchivedArticle
580 self.listing.addArchivedArticle(self.key, self.id)
582 def remove_archive_button(self, *widget):
583 self.set_for_removal = True
585 #def reloadArticle(self, *widget):
586 # if threading.activeCount() > 1:
587 # Image thread are still running, come back in a bit
590 # for (stream, imageThread) in self.images:
592 # stream.write(imageThread.data)
597 def _signal_link_clicked(self, object, link):
599 bus = dbus.SessionBus()
600 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
601 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
602 iface.open_new_window(link)
604 #def _signal_request_url(self, object, url, stream):
606 # self.imageDownloader.queueImage(url, stream)
607 #imageThread = GetImage(url)
609 #self.images.append((stream, imageThread))
612 class DisplayFeed(hildon.StackableWindow):
613 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
614 hildon.StackableWindow.__init__(self)
615 self.listing = listing
617 self.feedTitle = title
618 self.set_title(title)
621 self.updateDbusHandler = updateDbusHandler
623 self.downloadDialog = False
625 #self.listing.setCurrentlyDisplayedFeed(self.key)
629 menu = hildon.AppMenu()
630 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631 button.set_label("Update feed")
632 button.connect("clicked", self.button_update_clicked)
635 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
636 button.set_label("Mark all as read")
637 button.connect("clicked", self.buttonReadAllClicked)
640 if key=="ArchivedArticles":
641 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
642 button.set_label("Delete read articles")
643 button.connect("clicked", self.buttonPurgeArticles)
646 self.set_app_menu(menu)
651 self.connect('configure-event', self.on_configure_event)
652 self.connect("destroy", self.destroyWindow)
654 def on_configure_event(self, window, event):
655 if getattr(self, 'markup_renderer', None) is None:
658 # Fix up the column width for wrapping the text when the window is
659 # resized (i.e. orientation changed)
660 self.markup_renderer.set_property('wrap-width', event.width-20)
661 it = self.feedItems.get_iter_first()
662 while it is not None:
663 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
664 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
665 it = self.feedItems.iter_next(it)
667 def destroyWindow(self, *args):
668 #self.feed.saveUnread(CONFIGDIR)
669 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
670 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
671 self.emit("feed-closed", self.key)
673 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
674 #self.listing.closeCurrentlyDisplayedFeed()
676 def fix_title(self, title):
677 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
679 def displayFeed(self):
680 self.pannableFeed = hildon.PannableArea()
682 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
684 self.feedItems = gtk.ListStore(str, str)
685 #self.feedList = gtk.TreeView(self.feedItems)
686 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
687 selection = self.feedList.get_selection()
688 selection.set_mode(gtk.SELECTION_NONE)
689 #selection.connect("changed", lambda w: True)
691 self.feedList.set_model(self.feedItems)
692 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
695 self.feedList.set_hover_selection(False)
696 #self.feedList.set_property('enable-grid-lines', True)
697 #self.feedList.set_property('hildon-mode', 1)
698 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
700 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
702 vbox= gtk.VBox(False, 10)
703 vbox.pack_start(self.feedList)
705 self.pannableFeed.add_with_viewport(vbox)
707 self.markup_renderer = gtk.CellRendererText()
708 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
709 (width, height) = self.get_size()
710 self.markup_renderer.set_property('wrap-width', width-20)
711 self.markup_renderer.set_property('ypad', 5)
712 self.markup_renderer.set_property('xpad', 5)
713 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
714 markup=FEED_COLUMN_MARKUP)
715 self.feedList.append_column(markup_column)
717 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
718 hideReadArticles = self.config.getHideReadArticles()
720 for id in self.feed.getIds():
723 isRead = self.feed.isEntryRead(id)
726 if not ( isRead and hideReadArticles ):
727 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
728 #title = self.feed.getTitle(id)
729 title = self.fix_title(self.feed.getTitle(id))
731 #if self.feed.isEntryRead(id):
733 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
735 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
737 self.feedItems.append((markup, id))
740 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
742 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
743 self.feedItems.append((markup, ""))
745 self.add(self.pannableFeed)
749 self.pannableFeed.destroy()
750 #self.remove(self.pannableFeed)
752 def on_feedList_row_activated(self, treeview, path): #, column):
753 selection = self.feedList.get_selection()
754 selection.set_mode(gtk.SELECTION_SINGLE)
755 self.feedList.get_selection().select_path(path)
756 model = treeview.get_model()
757 iter = model.get_iter(path)
758 key = model.get_value(iter, FEED_COLUMN_KEY)
759 # Emulate legacy "button_clicked" call via treeview
760 gobject.idle_add(self.button_clicked, treeview, key)
763 def button_clicked(self, button, index, previous=False, next=False):
764 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
765 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
766 stack = hildon.WindowStack.get_default()
769 stack.pop_and_push(1, newDisp, tmp)
771 gobject.timeout_add(200, self.destroyArticle, tmp)
776 if type(self.disp).__name__ == "DisplayArticle":
777 gobject.timeout_add(200, self.destroyArticle, self.disp)
784 if self.key == "ArchivedArticles":
785 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
786 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
787 self.ids.append(self.disp.connect("article-next", self.nextArticle))
788 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
790 def buttonPurgeArticles(self, *widget):
792 self.feed.purgeReadArticles()
793 self.feed.saveUnread(CONFIGDIR)
794 self.feed.saveFeed(CONFIGDIR)
797 def destroyArticle(self, handle):
798 handle.destroyWindow()
800 def mark_item_read(self, key):
801 it = self.feedItems.get_iter_first()
802 while it is not None:
803 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
805 title = self.fix_title(self.feed.getTitle(key))
806 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
807 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
809 it = self.feedItems.iter_next(it)
811 def nextArticle(self, object, index):
812 self.mark_item_read(index)
813 id = self.feed.getNextId(index)
814 if self.config.getHideReadArticles():
817 isRead = self.feed.isEntryRead(id)
820 while isRead and id != index:
821 id = self.feed.getNextId(id)
824 isRead = self.feed.isEntryRead(id)
828 self.button_clicked(object, id, next=True)
830 def previousArticle(self, object, index):
831 self.mark_item_read(index)
832 id = self.feed.getPreviousId(index)
833 if self.config.getHideReadArticles():
836 isRead = self.feed.isEntryRead(id)
839 while isRead and id != index:
840 id = self.feed.getPreviousId(id)
843 isRead = self.feed.isEntryRead(id)
847 self.button_clicked(object, id, previous=True)
849 def onArticleClosed(self, object, index):
850 selection = self.feedList.get_selection()
851 selection.set_mode(gtk.SELECTION_NONE)
852 self.mark_item_read(index)
854 def onArticleDeleted(self, object, index):
856 self.feed.removeArticle(index)
857 self.feed.saveUnread(CONFIGDIR)
858 self.feed.saveFeed(CONFIGDIR)
861 def button_update_clicked(self, button):
862 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
863 if not type(self.downloadDialog).__name__=="DownloadBar":
864 self.pannableFeed.destroy()
865 self.vbox = gtk.VBox(False, 10)
866 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
867 self.downloadDialog.connect("download-done", self.onDownloadsDone)
868 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
872 def onDownloadsDone(self, *widget):
874 self.feed = self.listing.getFeed(self.key)
876 self.updateDbusHandler.ArticleCountUpdated()
878 def buttonReadAllClicked(self, button):
879 for index in self.feed.getIds():
880 self.feed.setEntryRead(index)
881 self.mark_item_read(index)
887 self.window = hildon.StackableWindow()
888 self.window.set_title(__appname__)
889 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
890 self.mainVbox = gtk.VBox(False,10)
892 self.introLabel = gtk.Label("Loading...")
895 self.mainVbox.pack_start(self.introLabel)
897 self.window.add(self.mainVbox)
898 self.window.show_all()
899 self.config = Config(self.window, CONFIGDIR+"config.ini")
900 gobject.idle_add(self.createWindow)
902 def createWindow(self):
903 self.app_lock = get_lock("app_lock")
904 if self.app_lock == None:
905 self.introLabel.set_label("Update in progress, please wait.")
906 gobject.timeout_add_seconds(3, self.createWindow)
908 self.listing = Listing(CONFIGDIR)
910 self.downloadDialog = False
912 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
913 self.orientation.set_mode(self.config.getOrientation())
915 print "Could not start rotation manager"
917 menu = hildon.AppMenu()
918 # Create a button and add it to the menu
919 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
920 button.set_label("Update feeds")
921 button.connect("clicked", self.button_update_clicked, "All")
924 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925 button.set_label("Mark all as read")
926 button.connect("clicked", self.button_markAll)
929 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930 button.set_label("Manage subscriptions")
931 button.connect("clicked", self.button_organize_clicked)
934 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
935 button.set_label("Settings")
936 button.connect("clicked", self.button_preferences_clicked)
939 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
940 button.set_label("About")
941 button.connect("clicked", self.button_about_clicked)
944 self.window.set_app_menu(menu)
947 #self.feedWindow = hildon.StackableWindow()
948 #self.articleWindow = hildon.StackableWindow()
949 self.introLabel.destroy()
950 self.pannableListing = hildon.PannableArea()
951 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
952 self.feedList = gtk.TreeView(self.feedItems)
953 self.feedList.connect('row-activated', self.on_feedList_row_activated)
954 self.pannableListing.add(self.feedList)
956 icon_renderer = gtk.CellRendererPixbuf()
957 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
958 icon_column = gtk.TreeViewColumn('', icon_renderer, \
960 self.feedList.append_column(icon_column)
962 markup_renderer = gtk.CellRendererText()
963 markup_column = gtk.TreeViewColumn('', markup_renderer, \
964 markup=COLUMN_MARKUP)
965 self.feedList.append_column(markup_column)
966 self.mainVbox.pack_start(self.pannableListing)
967 self.mainVbox.show_all()
969 self.displayListing()
970 self.autoupdate = False
971 self.checkAutoUpdate()
972 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
973 gobject.idle_add(self.enableDbus)
975 def enableDbus(self):
976 self.dbusHandler = ServerObject(self)
977 self.updateDbusHandler = UpdateServerObject(self)
979 def button_markAll(self, button):
980 for key in self.listing.getListOfFeeds():
981 feed = self.listing.getFeed(key)
982 for id in feed.getIds():
983 feed.setEntryRead(id)
984 feed.saveUnread(CONFIGDIR)
985 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
986 self.displayListing()
988 def button_about_clicked(self, button):
989 HeAboutDialog.present(self.window, \
999 def button_export_clicked(self, button):
1000 opml = ExportOpmlData(self.window, self.listing)
1002 def button_import_clicked(self, button):
1003 opml = GetOpmlData(self.window)
1004 feeds = opml.getData()
1005 for (title, url) in feeds:
1006 self.listing.addFeed(title, url)
1007 self.displayListing()
1009 def addFeed(self, urlIn="http://"):
1010 wizard = AddWidgetWizard(self.window, urlIn)
1013 (title, url) = wizard.getData()
1014 if (not title == '') and (not url == ''):
1015 self.listing.addFeed(title, url)
1017 self.displayListing()
1019 def button_organize_clicked(self, button):
1020 def after_closing():
1021 self.listing.saveConfig()
1022 self.displayListing()
1023 SortList(self.window, self.listing, self, after_closing)
1025 def button_update_clicked(self, button, key):
1026 if not type(self.downloadDialog).__name__=="DownloadBar":
1027 self.updateDbusHandler.UpdateStarted()
1028 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1029 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1030 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1031 self.mainVbox.show_all()
1032 #self.displayListing()
1034 def onDownloadsDone(self, *widget):
1035 self.downloadDialog.destroy()
1036 self.downloadDialog = False
1037 self.displayListing()
1038 self.updateDbusHandler.UpdateFinished()
1039 self.updateDbusHandler.ArticleCountUpdated()
1041 def button_preferences_clicked(self, button):
1042 dialog = self.config.createDialog()
1043 dialog.connect("destroy", self.prefsClosed)
1045 def show_confirmation_note(self, parent, title):
1046 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1048 retcode = gtk.Dialog.run(note)
1051 if retcode == gtk.RESPONSE_OK:
1056 def displayListing(self):
1057 icon_theme = gtk.icon_theme_get_default()
1058 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1059 gtk.ICON_LOOKUP_USE_BUILTIN)
1061 self.feedItems.clear()
1062 for key in self.listing.getListOfFeeds():
1063 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1064 if unreadItems > 0 or not self.config.getHideReadFeeds():
1065 title = self.listing.getFeedTitle(key)
1066 updateTime = self.listing.getFeedUpdateTime(key)
1068 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1071 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1073 markup = FEED_TEMPLATE % (title, subtitle)
1076 icon_filename = self.listing.getFavicon(key)
1077 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1078 LIST_ICON_SIZE, LIST_ICON_SIZE)
1080 pixbuf = default_pixbuf
1082 self.feedItems.append((pixbuf, markup, key))
1084 def on_feedList_row_activated(self, treeview, path, column):
1085 model = treeview.get_model()
1086 iter = model.get_iter(path)
1087 key = model.get_value(iter, COLUMN_KEY)
1090 def openFeed(self, key):
1094 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1095 self.feed_lock = get_lock(key)
1096 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1097 self.listing.getFeedTitle(key), key, \
1098 self.config, self.updateDbusHandler)
1099 self.disp.connect("feed-closed", self.onFeedClosed)
1102 def onFeedClosed(self, object, key):
1103 #self.listing.saveConfig()
1105 gobject.idle_add(self.onFeedClosedTimeout)
1106 self.displayListing()
1107 #self.updateDbusHandler.ArticleCountUpdated()
1109 def onFeedClosedTimeout(self):
1110 self.listing.saveConfig()
1112 self.updateDbusHandler.ArticleCountUpdated()
1115 self.window.connect("destroy", gtk.main_quit)
1117 self.listing.saveConfig()
1120 def prefsClosed(self, *widget):
1122 self.orientation.set_mode(self.config.getOrientation())
1125 self.displayListing()
1126 self.checkAutoUpdate()
1128 def checkAutoUpdate(self, *widget):
1129 interval = int(self.config.getUpdateInterval()*3600000)
1130 if self.config.isAutoUpdateEnabled():
1131 if self.autoupdate == False:
1132 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1133 self.autoupdate = interval
1134 elif not self.autoupdate == interval:
1135 # If auto-update is enabled, but not at the right frequency
1136 gobject.source_remove(self.autoupdateId)
1137 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1138 self.autoupdate = interval
1140 if not self.autoupdate == False:
1141 gobject.source_remove(self.autoupdateId)
1142 self.autoupdate = False
1144 def automaticUpdate(self, *widget):
1145 # Need to check for internet connection
1146 # If no internet connection, try again in 10 minutes:
1147 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1148 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1149 #from time import localtime, strftime
1150 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1152 self.button_update_clicked(None, None)
1155 def stopUpdate(self):
1156 # Not implemented in the app (see update_feeds.py)
1158 self.downloadDialog.listOfKeys = []
1162 def getStatus(self):
1164 for key in self.listing.getListOfFeeds():
1165 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1166 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1168 status = "No unread items"
1171 if __name__ == "__main__":
1172 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1173 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1174 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1175 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1176 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1177 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1178 gobject.threads_init()
1179 if not isdir(CONFIGDIR):
1183 print "Error: Can't create configuration directory"
1184 from sys import exit