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)
234 opener.addheaders = [('User-agent', USER_AGENT)]
235 install_opener(opener)
237 opener = build_opener()
238 opener.addheaders = [('User-agent', USER_AGENT)]
239 install_opener(opener)
242 self.set_text("Updating...")
244 self.set_fraction(self.fraction)
247 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
249 def update_progress_bar(self):
250 #self.progress_bar.pulse()
251 if activeCount() < 4:
252 x = activeCount() - 1
253 k = len(self.listOfKeys)
254 fin = self.total - k - x
255 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
256 #print x, k, fin, fraction
257 self.set_fraction(fraction)
259 if len(self.listOfKeys)>0:
260 self.current = self.current+1
261 key = self.listOfKeys.pop()
262 #if self.single == True:
263 # Check if the feed is being displayed
264 download = Download(self.listing, key, self.config)
267 elif activeCount() > 1:
270 #self.waitingWindow.destroy()
276 self.emit("download-done", "success")
281 class SortList(hildon.StackableWindow):
282 def __init__(self, parent, listing, feedingit, after_closing):
283 hildon.StackableWindow.__init__(self)
284 self.set_transient_for(parent)
285 self.set_title('Subscriptions')
286 self.listing = listing
287 self.feedingit = feedingit
288 self.after_closing = after_closing
289 self.connect('destroy', lambda w: self.after_closing())
290 self.vbox2 = gtk.VBox(False, 2)
292 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
293 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
294 button.connect("clicked", self.buttonUp)
295 self.vbox2.pack_start(button, expand=False, fill=False)
297 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
298 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
299 button.connect("clicked", self.buttonDown)
300 self.vbox2.pack_start(button, expand=False, fill=False)
302 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
304 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
305 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
306 button.connect("clicked", self.buttonAdd)
307 self.vbox2.pack_start(button, expand=False, fill=False)
309 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
310 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
311 button.connect("clicked", self.buttonEdit)
312 self.vbox2.pack_start(button, expand=False, fill=False)
314 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
315 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
316 button.connect("clicked", self.buttonDelete)
317 self.vbox2.pack_start(button, expand=False, fill=False)
319 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
320 #button.set_label("Done")
321 #button.connect("clicked", self.buttonDone)
322 #self.vbox.pack_start(button)
323 self.hbox2= gtk.HBox(False, 10)
324 self.pannableArea = hildon.PannableArea()
325 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
326 self.treeview = gtk.TreeView(self.treestore)
327 self.hbox2.pack_start(self.pannableArea, expand=True)
329 self.hbox2.pack_end(self.vbox2, expand=False)
330 self.set_default_size(-1, 600)
333 menu = hildon.AppMenu()
334 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
335 button.set_label("Import from OPML")
336 button.connect("clicked", self.feedingit.button_import_clicked)
339 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
340 button.set_label("Export to OPML")
341 button.connect("clicked", self.feedingit.button_export_clicked)
343 self.set_app_menu(menu)
347 #self.connect("destroy", self.buttonDone)
349 def displayFeeds(self):
350 self.treeview.destroy()
351 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
352 self.treeview = gtk.TreeView()
354 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
355 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
357 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
359 self.pannableArea.add(self.treeview)
363 def refreshList(self, selected=None, offset=0):
364 #rect = self.treeview.get_visible_rect()
365 #y = rect.y+rect.height
366 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
367 for key in self.listing.getListOfFeeds():
368 item = self.treestore.append([self.listing.getFeedTitle(key), key])
371 self.treeview.set_model(self.treestore)
372 if not selected == None:
373 self.treeview.get_selection().select_iter(selectedItem)
374 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
375 self.pannableArea.show_all()
377 def getSelectedItem(self):
378 (model, iter) = self.treeview.get_selection().get_selected()
381 return model.get_value(iter, 1)
383 def findIndex(self, key):
387 for row in self.treestore:
389 return (before, row.iter)
390 if key == list(row)[0]:
394 return (before, None)
396 def buttonUp(self, button):
397 key = self.getSelectedItem()
399 self.listing.moveUp(key)
400 self.refreshList(key, -10)
402 def buttonDown(self, button):
403 key = self.getSelectedItem()
405 self.listing.moveDown(key)
406 self.refreshList(key, 10)
408 def buttonDelete(self, button):
409 key = self.getSelectedItem()
411 self.listing.removeFeed(key)
414 def buttonEdit(self, button):
415 key = self.getSelectedItem()
417 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
420 (title, url) = wizard.getData()
421 if (not title == '') and (not url == ''):
422 self.listing.editFeed(key, title, url)
426 def buttonDone(self, *args):
429 def buttonAdd(self, button, urlIn="http://"):
430 wizard = AddWidgetWizard(self, urlIn)
433 (title, url) = wizard.getData()
434 if (not title == '') and (not url == ''):
435 self.listing.addFeed(title, url)
440 class DisplayArticle(hildon.StackableWindow):
441 def __init__(self, feed, id, key, config, listing):
442 hildon.StackableWindow.__init__(self)
443 #self.imageDownloader = ImageDownloader()
448 #self.set_title(feed.getTitle(id))
449 self.set_title(self.listing.getFeedTitle(key))
451 self.set_for_removal = False
453 # Init the article display
454 #if self.config.getWebkitSupport():
455 self.view = WebView()
456 #self.view.set_editable(False)
459 # self.view = gtkhtml2.View()
460 # self.document = gtkhtml2.Document()
461 # self.view.set_document(self.document)
462 # self.document.connect("link_clicked", self._signal_link_clicked)
463 self.pannable_article = hildon.PannableArea()
464 self.pannable_article.add(self.view)
465 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
466 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
468 #if self.config.getWebkitSupport():
469 contentLink = self.feed.getContentLink(self.id)
470 self.feed.setEntryRead(self.id)
471 #if key=="ArchivedArticles":
472 if contentLink.startswith("/home/user/"):
473 self.view.open("file://" + contentLink)
475 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
476 self.view.connect("motion-notify-event", lambda w,ev: True)
477 self.view.connect('load-started', self.load_started)
478 self.view.connect('load-finished', self.load_finished)
481 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
482 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
484 # if not key == "ArchivedArticles":
485 # Do not download images if the feed is "Archived Articles"
486 # self.document.connect("request-url", self._signal_request_url)
488 # self.document.clear()
489 # self.document.open_stream("text/html")
490 # self.document.write_stream(self.text)
491 # self.document.close_stream()
493 menu = hildon.AppMenu()
494 # Create a button and add it to the menu
495 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
496 button.set_label("Allow Horizontal Scrolling")
497 button.connect("clicked", self.horiz_scrolling_button)
500 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
501 button.set_label("Open in Browser")
502 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
505 if key == "ArchivedArticles":
506 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
507 button.set_label("Remove from Archived Articles")
508 button.connect("clicked", self.remove_archive_button)
510 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
511 button.set_label("Add to Archived Articles")
512 button.connect("clicked", self.archive_button)
515 self.set_app_menu(menu)
518 #self.event_box = gtk.EventBox()
519 #self.event_box.add(self.pannable_article)
520 self.add(self.pannable_article)
523 self.pannable_article.show_all()
525 self.destroyId = self.connect("destroy", self.destroyWindow)
527 self.view.connect("button_press_event", self.button_pressed)
528 self.gestureId = self.view.connect("button_release_event", self.button_released)
529 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
531 def load_started(self, *widget):
532 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
534 def load_finished(self, *widget):
535 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
537 def button_pressed(self, window, event):
538 #print event.x, event.y
539 self.coords = (event.x, event.y)
541 def button_released(self, window, event):
542 x = self.coords[0] - event.x
543 y = self.coords[1] - event.y
545 if (2*abs(y) < abs(x)):
547 self.emit("article-previous", self.id)
549 self.emit("article-next", self.id)
553 #def gesture(self, widget, direction, startx, starty):
554 # if (direction == 3):
555 # self.emit("article-next", self.index)
556 # if (direction == 2):
557 # self.emit("article-previous", self.index)
558 #print startx, starty
559 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
561 def destroyWindow(self, *args):
562 self.disconnect(self.destroyId)
563 if self.set_for_removal:
564 self.emit("article-deleted", self.id)
566 self.emit("article-closed", self.id)
567 #self.imageDownloader.stopAll()
570 def horiz_scrolling_button(self, *widget):
571 self.pannable_article.disconnect(self.gestureId)
572 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
574 def archive_button(self, *widget):
575 # Call the listing.addArchivedArticle
576 self.listing.addArchivedArticle(self.key, self.id)
578 def remove_archive_button(self, *widget):
579 self.set_for_removal = True
581 #def reloadArticle(self, *widget):
582 # if threading.activeCount() > 1:
583 # Image thread are still running, come back in a bit
586 # for (stream, imageThread) in self.images:
588 # stream.write(imageThread.data)
593 def _signal_link_clicked(self, object, link):
595 bus = dbus.SessionBus()
596 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
597 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
598 iface.open_new_window(link)
600 #def _signal_request_url(self, object, url, stream):
602 # self.imageDownloader.queueImage(url, stream)
603 #imageThread = GetImage(url)
605 #self.images.append((stream, imageThread))
608 class DisplayFeed(hildon.StackableWindow):
609 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
610 hildon.StackableWindow.__init__(self)
611 self.listing = listing
613 self.feedTitle = title
614 self.set_title(title)
617 self.updateDbusHandler = updateDbusHandler
619 self.downloadDialog = False
621 #self.listing.setCurrentlyDisplayedFeed(self.key)
625 menu = hildon.AppMenu()
626 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
627 button.set_label("Update Feed")
628 button.connect("clicked", self.button_update_clicked)
631 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
632 button.set_label("Mark All As Read")
633 button.connect("clicked", self.buttonReadAllClicked)
636 if key=="ArchivedArticles":
637 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
638 button.set_label("Purge Read Articles")
639 button.connect("clicked", self.buttonPurgeArticles)
642 self.set_app_menu(menu)
647 self.connect('configure-event', self.on_configure_event)
648 self.connect("destroy", self.destroyWindow)
650 def on_configure_event(self, window, event):
651 if getattr(self, 'markup_renderer', None) is None:
654 # Fix up the column width for wrapping the text when the window is
655 # resized (i.e. orientation changed)
656 self.markup_renderer.set_property('wrap-width', event.width-10)
658 def destroyWindow(self, *args):
659 #self.feed.saveUnread(CONFIGDIR)
660 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
661 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
662 self.emit("feed-closed", self.key)
664 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
665 #self.listing.closeCurrentlyDisplayedFeed()
667 def fix_title(self, title):
668 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
670 def displayFeed(self):
671 self.pannableFeed = hildon.PannableArea()
673 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
675 self.feedItems = gtk.ListStore(str, str)
676 #self.feedList = gtk.TreeView(self.feedItems)
677 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
678 selection = self.feedList.get_selection()
679 selection.set_mode(gtk.SELECTION_NONE)
680 #selection.connect("changed", lambda w: True)
682 self.feedList.set_model(self.feedItems)
683 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
686 self.feedList.set_hover_selection(False)
687 #self.feedList.set_property('enable-grid-lines', True)
688 #self.feedList.set_property('hildon-mode', 1)
689 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
691 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
693 vbox= gtk.VBox(False, 10)
694 vbox.pack_start(self.feedList)
696 self.pannableFeed.add_with_viewport(vbox)
698 self.markup_renderer = gtk.CellRendererText()
699 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
700 self.markup_renderer.set_property('wrap-width', 780)
701 self.markup_renderer.set_property('ypad', 5)
702 self.markup_renderer.set_property('xpad', 5)
703 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
704 markup=FEED_COLUMN_MARKUP)
705 self.feedList.append_column(markup_column)
707 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
708 hideReadArticles = self.config.getHideReadArticles()
710 for id in self.feed.getIds():
713 isRead = self.feed.isEntryRead(id)
716 if not ( isRead and hideReadArticles ):
717 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
718 #title = self.feed.getTitle(id)
719 title = self.fix_title(self.feed.getTitle(id))
721 #if self.feed.isEntryRead(id):
723 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
725 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
727 self.feedItems.append((markup, id))
730 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
732 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
733 self.feedItems.append((markup, ""))
735 self.add(self.pannableFeed)
739 self.pannableFeed.destroy()
740 #self.remove(self.pannableFeed)
742 def on_feedList_row_activated(self, treeview, path): #, column):
743 selection = self.feedList.get_selection()
744 selection.set_mode(gtk.SELECTION_SINGLE)
745 self.feedList.get_selection().select_path(path)
746 model = treeview.get_model()
747 iter = model.get_iter(path)
748 key = model.get_value(iter, FEED_COLUMN_KEY)
749 # Emulate legacy "button_clicked" call via treeview
750 gobject.idle_add(self.button_clicked, treeview, key)
753 def button_clicked(self, button, index, previous=False, next=False):
754 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
755 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
756 stack = hildon.WindowStack.get_default()
759 stack.pop_and_push(1, newDisp, tmp)
761 gobject.timeout_add(200, self.destroyArticle, tmp)
766 if type(self.disp).__name__ == "DisplayArticle":
767 gobject.timeout_add(200, self.destroyArticle, self.disp)
774 if self.key == "ArchivedArticles":
775 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
776 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
777 self.ids.append(self.disp.connect("article-next", self.nextArticle))
778 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
780 def buttonPurgeArticles(self, *widget):
782 self.feed.purgeReadArticles()
783 self.feed.saveUnread(CONFIGDIR)
784 self.feed.saveFeed(CONFIGDIR)
787 def destroyArticle(self, handle):
788 handle.destroyWindow()
790 def mark_item_read(self, key):
791 it = self.feedItems.get_iter_first()
792 while it is not None:
793 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
795 title = self.fix_title(self.feed.getTitle(key))
796 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
797 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
799 it = self.feedItems.iter_next(it)
801 def nextArticle(self, object, index):
802 self.mark_item_read(index)
803 id = self.feed.getNextId(index)
804 if self.config.getHideReadArticles():
807 isRead = self.feed.isEntryRead(id)
810 while isRead and id != index:
811 id = self.feed.getNextId(id)
814 isRead = self.feed.isEntryRead(id)
818 self.button_clicked(object, id, next=True)
820 def previousArticle(self, object, index):
821 self.mark_item_read(index)
822 id = self.feed.getPreviousId(index)
823 if self.config.getHideReadArticles():
826 isRead = self.feed.isEntryRead(id)
829 while isRead and id != index:
830 id = self.feed.getPreviousId(id)
833 isRead = self.feed.isEntryRead(id)
837 self.button_clicked(object, id, previous=True)
839 def onArticleClosed(self, object, index):
840 selection = self.feedList.get_selection()
841 selection.set_mode(gtk.SELECTION_NONE)
842 self.mark_item_read(index)
844 def onArticleDeleted(self, object, index):
846 self.feed.removeArticle(index)
847 self.feed.saveUnread(CONFIGDIR)
848 self.feed.saveFeed(CONFIGDIR)
851 def button_update_clicked(self, button):
852 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
853 if not type(self.downloadDialog).__name__=="DownloadBar":
854 self.pannableFeed.destroy()
855 self.vbox = gtk.VBox(False, 10)
856 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
857 self.downloadDialog.connect("download-done", self.onDownloadsDone)
858 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
862 def onDownloadsDone(self, *widget):
864 self.feed = self.listing.getFeed(self.key)
866 self.updateDbusHandler.ArticleCountUpdated()
868 def buttonReadAllClicked(self, button):
869 for index in self.feed.getIds():
870 self.feed.setEntryRead(index)
871 self.mark_item_read(index)
877 self.window = hildon.StackableWindow()
878 self.window.set_title(__appname__)
879 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
880 self.mainVbox = gtk.VBox(False,10)
882 self.introLabel = gtk.Label("Loading...")
885 self.mainVbox.pack_start(self.introLabel)
887 self.window.add(self.mainVbox)
888 self.window.show_all()
889 self.config = Config(self.window, CONFIGDIR+"config.ini")
890 gobject.idle_add(self.createWindow)
892 def createWindow(self):
893 self.app_lock = get_lock("app_lock")
894 if self.app_lock == None:
895 self.introLabel.set_label("Update in progress, please wait.")
896 gobject.timeout_add_seconds(3, self.createWindow)
898 self.listing = Listing(CONFIGDIR)
900 self.downloadDialog = False
902 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
903 self.orientation.set_mode(self.config.getOrientation())
905 print "Could not start rotation manager"
907 menu = hildon.AppMenu()
908 # Create a button and add it to the menu
909 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
910 button.set_label("Update All Feeds")
911 button.connect("clicked", self.button_update_clicked, "All")
914 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
915 button.set_label("Mark All As Read")
916 button.connect("clicked", self.button_markAll)
919 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
920 button.set_label("Manage subscriptions")
921 button.connect("clicked", self.button_organize_clicked)
924 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
925 button.set_label("Settings")
926 button.connect("clicked", self.button_preferences_clicked)
929 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
930 button.set_label("About")
931 button.connect("clicked", self.button_about_clicked)
934 self.window.set_app_menu(menu)
937 #self.feedWindow = hildon.StackableWindow()
938 #self.articleWindow = hildon.StackableWindow()
939 self.introLabel.destroy()
940 self.pannableListing = hildon.PannableArea()
941 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
942 self.feedList = gtk.TreeView(self.feedItems)
943 self.feedList.connect('row-activated', self.on_feedList_row_activated)
944 self.pannableListing.add(self.feedList)
946 icon_renderer = gtk.CellRendererPixbuf()
947 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
948 icon_column = gtk.TreeViewColumn('', icon_renderer, \
950 self.feedList.append_column(icon_column)
952 markup_renderer = gtk.CellRendererText()
953 markup_column = gtk.TreeViewColumn('', markup_renderer, \
954 markup=COLUMN_MARKUP)
955 self.feedList.append_column(markup_column)
956 self.mainVbox.pack_start(self.pannableListing)
957 self.mainVbox.show_all()
959 self.displayListing()
960 self.autoupdate = False
961 self.checkAutoUpdate()
962 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
963 gobject.idle_add(self.enableDbus)
965 def enableDbus(self):
966 self.dbusHandler = ServerObject(self)
967 self.updateDbusHandler = UpdateServerObject(self)
969 def button_markAll(self, button):
970 for key in self.listing.getListOfFeeds():
971 feed = self.listing.getFeed(key)
972 for id in feed.getIds():
973 feed.setEntryRead(id)
974 feed.saveUnread(CONFIGDIR)
975 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
976 self.displayListing()
978 def button_about_clicked(self, button):
979 HeAboutDialog.present(self.window, \
989 def button_export_clicked(self, button):
990 opml = ExportOpmlData(self.window, self.listing)
992 def button_import_clicked(self, button):
993 opml = GetOpmlData(self.window)
994 feeds = opml.getData()
995 for (title, url) in feeds:
996 self.listing.addFeed(title, url)
997 self.displayListing()
999 def addFeed(self, urlIn="http://"):
1000 wizard = AddWidgetWizard(self.window, urlIn)
1003 (title, url) = wizard.getData()
1004 if (not title == '') and (not url == ''):
1005 self.listing.addFeed(title, url)
1007 self.displayListing()
1009 def button_organize_clicked(self, button):
1010 def after_closing():
1011 self.listing.saveConfig()
1012 self.displayListing()
1013 SortList(self.window, self.listing, self, after_closing)
1015 def button_update_clicked(self, button, key):
1016 if not type(self.downloadDialog).__name__=="DownloadBar":
1017 self.updateDbusHandler.UpdateStarted()
1018 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1019 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1020 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1021 self.mainVbox.show_all()
1022 #self.displayListing()
1024 def onDownloadsDone(self, *widget):
1025 self.downloadDialog.destroy()
1026 self.downloadDialog = False
1027 self.displayListing()
1028 self.updateDbusHandler.UpdateFinished()
1029 self.updateDbusHandler.ArticleCountUpdated()
1031 def button_preferences_clicked(self, button):
1032 dialog = self.config.createDialog()
1033 dialog.connect("destroy", self.prefsClosed)
1035 def show_confirmation_note(self, parent, title):
1036 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1038 retcode = gtk.Dialog.run(note)
1041 if retcode == gtk.RESPONSE_OK:
1046 def displayListing(self):
1047 icon_theme = gtk.icon_theme_get_default()
1048 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1049 gtk.ICON_LOOKUP_USE_BUILTIN)
1051 self.feedItems.clear()
1052 for key in self.listing.getListOfFeeds():
1053 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1054 if unreadItems > 0 or not self.config.getHideReadFeeds():
1055 title = self.listing.getFeedTitle(key)
1056 updateTime = self.listing.getFeedUpdateTime(key)
1058 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1061 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1063 markup = FEED_TEMPLATE % (title, subtitle)
1066 icon_filename = self.listing.getFavicon(key)
1067 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1068 LIST_ICON_SIZE, LIST_ICON_SIZE)
1070 pixbuf = default_pixbuf
1072 self.feedItems.append((pixbuf, markup, key))
1074 def on_feedList_row_activated(self, treeview, path, column):
1075 model = treeview.get_model()
1076 iter = model.get_iter(path)
1077 key = model.get_value(iter, COLUMN_KEY)
1080 def openFeed(self, key):
1084 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1085 self.feed_lock = get_lock(key)
1086 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1087 self.listing.getFeedTitle(key), key, \
1088 self.config, self.updateDbusHandler)
1089 self.disp.connect("feed-closed", self.onFeedClosed)
1092 def onFeedClosed(self, object, key):
1093 #self.listing.saveConfig()
1095 gobject.idle_add(self.onFeedClosedTimeout)
1096 self.displayListing()
1097 #self.updateDbusHandler.ArticleCountUpdated()
1099 def onFeedClosedTimeout(self):
1100 self.listing.saveConfig()
1102 self.updateDbusHandler.ArticleCountUpdated()
1105 self.window.connect("destroy", gtk.main_quit)
1107 self.listing.saveConfig()
1110 def prefsClosed(self, *widget):
1112 self.orientation.set_mode(self.config.getOrientation())
1115 self.displayListing()
1116 self.checkAutoUpdate()
1118 def checkAutoUpdate(self, *widget):
1119 interval = int(self.config.getUpdateInterval()*3600000)
1120 if self.config.isAutoUpdateEnabled():
1121 if self.autoupdate == False:
1122 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1123 self.autoupdate = interval
1124 elif not self.autoupdate == interval:
1125 # If auto-update is enabled, but not at the right frequency
1126 gobject.source_remove(self.autoupdateId)
1127 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1128 self.autoupdate = interval
1130 if not self.autoupdate == False:
1131 gobject.source_remove(self.autoupdateId)
1132 self.autoupdate = False
1134 def automaticUpdate(self, *widget):
1135 # Need to check for internet connection
1136 # If no internet connection, try again in 10 minutes:
1137 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1138 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1139 #from time import localtime, strftime
1140 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1142 self.button_update_clicked(None, None)
1145 def stopUpdate(self):
1146 # Not implemented in the app (see update_feeds.py)
1148 self.downloadDialog.listOfKeys = []
1152 def getStatus(self):
1154 for key in self.listing.getListOfFeeds():
1155 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1156 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1158 status = "No unread items"
1161 if __name__ == "__main__":
1162 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1163 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1164 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1165 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1166 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1167 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1168 gobject.threads_init()
1169 if not isdir(CONFIGDIR):
1173 print "Error: Can't create configuration directory"
1174 from sys import exit