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 if key == 'ArchivedArticles':
416 message = 'Cannot remove the archived articles feed.'
417 hildon.hildon_banner_show_information(self, '', message)
418 elif key is not None:
419 message = 'Really remove this feed and its entries?'
420 dlg = hildon.hildon_note_new_confirmation(self, message)
423 if response == gtk.RESPONSE_OK:
424 self.listing.removeFeed(key)
427 def buttonEdit(self, button):
428 key = self.getSelectedItem()
430 if key == 'ArchivedArticles':
431 message = 'Cannot edit the archived articles feed.'
432 hildon.hildon_banner_show_information(self, '', message)
436 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key), True)
439 (title, url) = wizard.getData()
440 if (not title == '') and (not url == ''):
441 self.listing.editFeed(key, title, url)
445 def buttonDone(self, *args):
448 def buttonAdd(self, button, urlIn="http://"):
449 wizard = AddWidgetWizard(self, urlIn)
452 (title, url) = wizard.getData()
453 if (not title == '') and (not url == ''):
454 self.listing.addFeed(title, url)
459 class DisplayArticle(hildon.StackableWindow):
460 def __init__(self, feed, id, key, config, listing):
461 hildon.StackableWindow.__init__(self)
462 #self.imageDownloader = ImageDownloader()
467 #self.set_title(feed.getTitle(id))
468 self.set_title(self.listing.getFeedTitle(key))
470 self.set_for_removal = False
472 # Init the article display
473 #if self.config.getWebkitSupport():
474 self.view = WebView()
475 #self.view.set_editable(False)
478 # self.view = gtkhtml2.View()
479 # self.document = gtkhtml2.Document()
480 # self.view.set_document(self.document)
481 # self.document.connect("link_clicked", self._signal_link_clicked)
482 self.pannable_article = hildon.PannableArea()
483 self.pannable_article.add(self.view)
484 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
485 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
487 #if self.config.getWebkitSupport():
488 contentLink = self.feed.getContentLink(self.id)
489 self.feed.setEntryRead(self.id)
490 #if key=="ArchivedArticles":
491 if contentLink.startswith("/home/user/"):
492 self.view.open("file://" + contentLink)
494 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
495 self.view.connect("motion-notify-event", lambda w,ev: True)
496 self.view.connect('load-started', self.load_started)
497 self.view.connect('load-finished', self.load_finished)
500 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
501 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
503 # if not key == "ArchivedArticles":
504 # Do not download images if the feed is "Archived Articles"
505 # self.document.connect("request-url", self._signal_request_url)
507 # self.document.clear()
508 # self.document.open_stream("text/html")
509 # self.document.write_stream(self.text)
510 # self.document.close_stream()
512 menu = hildon.AppMenu()
513 # Create a button and add it to the menu
514 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
515 button.set_label("Allow horizontal scrolling")
516 button.connect("clicked", self.horiz_scrolling_button)
519 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
520 button.set_label("Open in browser")
521 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
524 if key == "ArchivedArticles":
525 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
526 button.set_label("Remove from archived articles")
527 button.connect("clicked", self.remove_archive_button)
529 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
530 button.set_label("Add to archived articles")
531 button.connect("clicked", self.archive_button)
534 self.set_app_menu(menu)
537 #self.event_box = gtk.EventBox()
538 #self.event_box.add(self.pannable_article)
539 self.add(self.pannable_article)
542 self.pannable_article.show_all()
544 self.destroyId = self.connect("destroy", self.destroyWindow)
546 self.view.connect("button_press_event", self.button_pressed)
547 self.gestureId = self.view.connect("button_release_event", self.button_released)
548 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
550 def load_started(self, *widget):
551 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
553 def load_finished(self, *widget):
554 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
556 def button_pressed(self, window, event):
557 #print event.x, event.y
558 self.coords = (event.x, event.y)
560 def button_released(self, window, event):
561 x = self.coords[0] - event.x
562 y = self.coords[1] - event.y
564 if (2*abs(y) < abs(x)):
566 self.emit("article-previous", self.id)
568 self.emit("article-next", self.id)
572 #def gesture(self, widget, direction, startx, starty):
573 # if (direction == 3):
574 # self.emit("article-next", self.index)
575 # if (direction == 2):
576 # self.emit("article-previous", self.index)
577 #print startx, starty
578 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
580 def destroyWindow(self, *args):
581 self.disconnect(self.destroyId)
582 if self.set_for_removal:
583 self.emit("article-deleted", self.id)
585 self.emit("article-closed", self.id)
586 #self.imageDownloader.stopAll()
589 def horiz_scrolling_button(self, *widget):
590 self.pannable_article.disconnect(self.gestureId)
591 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
593 def archive_button(self, *widget):
594 # Call the listing.addArchivedArticle
595 self.listing.addArchivedArticle(self.key, self.id)
597 def remove_archive_button(self, *widget):
598 self.set_for_removal = True
600 #def reloadArticle(self, *widget):
601 # if threading.activeCount() > 1:
602 # Image thread are still running, come back in a bit
605 # for (stream, imageThread) in self.images:
607 # stream.write(imageThread.data)
612 def _signal_link_clicked(self, object, link):
614 bus = dbus.SessionBus()
615 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
616 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
617 iface.open_new_window(link)
619 #def _signal_request_url(self, object, url, stream):
621 # self.imageDownloader.queueImage(url, stream)
622 #imageThread = GetImage(url)
624 #self.images.append((stream, imageThread))
627 class DisplayFeed(hildon.StackableWindow):
628 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
629 hildon.StackableWindow.__init__(self)
630 self.listing = listing
632 self.feedTitle = title
633 self.set_title(title)
636 self.updateDbusHandler = updateDbusHandler
638 self.downloadDialog = False
640 #self.listing.setCurrentlyDisplayedFeed(self.key)
644 menu = hildon.AppMenu()
645 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
646 button.set_label("Update feed")
647 button.connect("clicked", self.button_update_clicked)
650 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
651 button.set_label("Mark all as read")
652 button.connect("clicked", self.buttonReadAllClicked)
655 if key=="ArchivedArticles":
656 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
657 button.set_label("Delete read articles")
658 button.connect("clicked", self.buttonPurgeArticles)
661 self.set_app_menu(menu)
666 self.connect('configure-event', self.on_configure_event)
667 self.connect("destroy", self.destroyWindow)
669 def on_configure_event(self, window, event):
670 if getattr(self, 'markup_renderer', None) is None:
673 # Fix up the column width for wrapping the text when the window is
674 # resized (i.e. orientation changed)
675 self.markup_renderer.set_property('wrap-width', event.width-20)
676 it = self.feedItems.get_iter_first()
677 while it is not None:
678 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
679 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
680 it = self.feedItems.iter_next(it)
682 def destroyWindow(self, *args):
683 #self.feed.saveUnread(CONFIGDIR)
684 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
685 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
686 self.emit("feed-closed", self.key)
688 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
689 #self.listing.closeCurrentlyDisplayedFeed()
691 def fix_title(self, title):
692 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
694 def displayFeed(self):
695 self.pannableFeed = hildon.PannableArea()
697 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
699 self.feedItems = gtk.ListStore(str, str)
700 #self.feedList = gtk.TreeView(self.feedItems)
701 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
702 selection = self.feedList.get_selection()
703 selection.set_mode(gtk.SELECTION_NONE)
704 #selection.connect("changed", lambda w: True)
706 self.feedList.set_model(self.feedItems)
707 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
710 self.feedList.set_hover_selection(False)
711 #self.feedList.set_property('enable-grid-lines', True)
712 #self.feedList.set_property('hildon-mode', 1)
713 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
715 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
717 vbox= gtk.VBox(False, 10)
718 vbox.pack_start(self.feedList)
720 self.pannableFeed.add_with_viewport(vbox)
722 self.markup_renderer = gtk.CellRendererText()
723 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
724 self.markup_renderer.set_property('background', "#333333")
725 (width, height) = self.get_size()
726 self.markup_renderer.set_property('wrap-width', width-20)
727 self.markup_renderer.set_property('ypad', 5)
728 self.markup_renderer.set_property('xpad', 5)
729 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
730 markup=FEED_COLUMN_MARKUP)
731 self.feedList.append_column(markup_column)
733 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
734 hideReadArticles = self.config.getHideReadArticles()
736 for id in self.feed.getIds():
739 isRead = self.feed.isEntryRead(id)
742 if not ( isRead and hideReadArticles ):
743 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
744 #title = self.feed.getTitle(id)
745 title = self.fix_title(self.feed.getTitle(id))
747 #if self.feed.isEntryRead(id):
749 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
751 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
753 self.feedItems.append((markup, id))
756 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
758 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
759 self.feedItems.append((markup, ""))
761 self.add(self.pannableFeed)
765 self.pannableFeed.destroy()
766 #self.remove(self.pannableFeed)
768 def on_feedList_row_activated(self, treeview, path): #, column):
769 selection = self.feedList.get_selection()
770 selection.set_mode(gtk.SELECTION_SINGLE)
771 self.feedList.get_selection().select_path(path)
772 model = treeview.get_model()
773 iter = model.get_iter(path)
774 key = model.get_value(iter, FEED_COLUMN_KEY)
775 # Emulate legacy "button_clicked" call via treeview
776 gobject.idle_add(self.button_clicked, treeview, key)
779 def button_clicked(self, button, index, previous=False, next=False):
780 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
781 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
782 stack = hildon.WindowStack.get_default()
785 stack.pop_and_push(1, newDisp, tmp)
787 gobject.timeout_add(200, self.destroyArticle, tmp)
792 if type(self.disp).__name__ == "DisplayArticle":
793 gobject.timeout_add(200, self.destroyArticle, self.disp)
800 if self.key == "ArchivedArticles":
801 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
802 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
803 self.ids.append(self.disp.connect("article-next", self.nextArticle))
804 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
806 def buttonPurgeArticles(self, *widget):
808 self.feed.purgeReadArticles()
809 self.feed.saveUnread(CONFIGDIR)
810 self.feed.saveFeed(CONFIGDIR)
813 def destroyArticle(self, handle):
814 handle.destroyWindow()
816 def mark_item_read(self, key):
817 it = self.feedItems.get_iter_first()
818 while it is not None:
819 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
821 title = self.fix_title(self.feed.getTitle(key))
822 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
823 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
825 it = self.feedItems.iter_next(it)
827 def nextArticle(self, object, index):
828 self.mark_item_read(index)
829 id = self.feed.getNextId(index)
830 if self.config.getHideReadArticles():
833 isRead = self.feed.isEntryRead(id)
836 while isRead and id != index:
837 id = self.feed.getNextId(id)
840 isRead = self.feed.isEntryRead(id)
844 self.button_clicked(object, id, next=True)
846 def previousArticle(self, object, index):
847 self.mark_item_read(index)
848 id = self.feed.getPreviousId(index)
849 if self.config.getHideReadArticles():
852 isRead = self.feed.isEntryRead(id)
855 while isRead and id != index:
856 id = self.feed.getPreviousId(id)
859 isRead = self.feed.isEntryRead(id)
863 self.button_clicked(object, id, previous=True)
865 def onArticleClosed(self, object, index):
866 selection = self.feedList.get_selection()
867 selection.set_mode(gtk.SELECTION_NONE)
868 self.mark_item_read(index)
870 def onArticleDeleted(self, object, index):
872 self.feed.removeArticle(index)
873 self.feed.saveUnread(CONFIGDIR)
874 self.feed.saveFeed(CONFIGDIR)
877 def button_update_clicked(self, button):
878 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
879 if not type(self.downloadDialog).__name__=="DownloadBar":
880 self.pannableFeed.destroy()
881 self.vbox = gtk.VBox(False, 10)
882 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
883 self.downloadDialog.connect("download-done", self.onDownloadsDone)
884 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
888 def onDownloadsDone(self, *widget):
890 self.feed = self.listing.getFeed(self.key)
892 self.updateDbusHandler.ArticleCountUpdated()
894 def buttonReadAllClicked(self, button):
895 for index in self.feed.getIds():
896 self.feed.setEntryRead(index)
897 self.mark_item_read(index)
903 self.window = hildon.StackableWindow()
904 self.window.set_title(__appname__)
905 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
906 self.mainVbox = gtk.VBox(False,10)
908 self.introLabel = gtk.Label("Loading...")
911 self.mainVbox.pack_start(self.introLabel)
913 self.window.add(self.mainVbox)
914 self.window.show_all()
915 self.config = Config(self.window, CONFIGDIR+"config.ini")
916 gobject.idle_add(self.createWindow)
918 def createWindow(self):
919 self.app_lock = get_lock("app_lock")
920 if self.app_lock == None:
921 self.introLabel.set_label("Update in progress, please wait.")
922 gobject.timeout_add_seconds(3, self.createWindow)
924 self.listing = Listing(CONFIGDIR)
926 self.downloadDialog = False
928 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
929 self.orientation.set_mode(self.config.getOrientation())
931 print "Could not start rotation manager"
933 menu = hildon.AppMenu()
934 # Create a button and add it to the menu
935 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
936 button.set_label("Update feeds")
937 button.connect("clicked", self.button_update_clicked, "All")
940 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
941 button.set_label("Mark all as read")
942 button.connect("clicked", self.button_markAll)
945 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
946 button.set_label("Manage subscriptions")
947 button.connect("clicked", self.button_organize_clicked)
950 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
951 button.set_label("Settings")
952 button.connect("clicked", self.button_preferences_clicked)
955 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
956 button.set_label("About")
957 button.connect("clicked", self.button_about_clicked)
960 self.window.set_app_menu(menu)
963 #self.feedWindow = hildon.StackableWindow()
964 #self.articleWindow = hildon.StackableWindow()
965 self.introLabel.destroy()
966 self.pannableListing = hildon.PannableArea()
967 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
968 self.feedList = gtk.TreeView(self.feedItems)
969 self.feedList.connect('row-activated', self.on_feedList_row_activated)
970 self.pannableListing.add(self.feedList)
972 icon_renderer = gtk.CellRendererPixbuf()
973 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
974 icon_column = gtk.TreeViewColumn('', icon_renderer, \
976 self.feedList.append_column(icon_column)
978 markup_renderer = gtk.CellRendererText()
979 markup_column = gtk.TreeViewColumn('', markup_renderer, \
980 markup=COLUMN_MARKUP)
981 self.feedList.append_column(markup_column)
982 self.mainVbox.pack_start(self.pannableListing)
983 self.mainVbox.show_all()
985 self.displayListing()
986 self.autoupdate = False
987 self.checkAutoUpdate()
988 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
989 gobject.idle_add(self.enableDbus)
991 def enableDbus(self):
992 self.dbusHandler = ServerObject(self)
993 self.updateDbusHandler = UpdateServerObject(self)
995 def button_markAll(self, button):
996 for key in self.listing.getListOfFeeds():
997 feed = self.listing.getFeed(key)
998 for id in feed.getIds():
999 feed.setEntryRead(id)
1000 feed.saveUnread(CONFIGDIR)
1001 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
1002 self.displayListing()
1004 def button_about_clicked(self, button):
1005 HeAboutDialog.present(self.window, \
1015 def button_export_clicked(self, button):
1016 opml = ExportOpmlData(self.window, self.listing)
1018 def button_import_clicked(self, button):
1019 opml = GetOpmlData(self.window)
1020 feeds = opml.getData()
1021 for (title, url) in feeds:
1022 self.listing.addFeed(title, url)
1023 self.displayListing()
1025 def addFeed(self, urlIn="http://"):
1026 wizard = AddWidgetWizard(self.window, urlIn)
1029 (title, url) = wizard.getData()
1030 if (not title == '') and (not url == ''):
1031 self.listing.addFeed(title, url)
1033 self.displayListing()
1035 def button_organize_clicked(self, button):
1036 def after_closing():
1037 self.listing.saveConfig()
1038 self.displayListing()
1039 SortList(self.window, self.listing, self, after_closing)
1041 def button_update_clicked(self, button, key):
1042 if not type(self.downloadDialog).__name__=="DownloadBar":
1043 self.updateDbusHandler.UpdateStarted()
1044 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1045 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1046 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1047 self.mainVbox.show_all()
1048 #self.displayListing()
1050 def onDownloadsDone(self, *widget):
1051 self.downloadDialog.destroy()
1052 self.downloadDialog = False
1053 self.displayListing()
1054 self.updateDbusHandler.UpdateFinished()
1055 self.updateDbusHandler.ArticleCountUpdated()
1057 def button_preferences_clicked(self, button):
1058 dialog = self.config.createDialog()
1059 dialog.connect("destroy", self.prefsClosed)
1061 def show_confirmation_note(self, parent, title):
1062 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1064 retcode = gtk.Dialog.run(note)
1067 if retcode == gtk.RESPONSE_OK:
1072 def displayListing(self):
1073 icon_theme = gtk.icon_theme_get_default()
1074 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1075 gtk.ICON_LOOKUP_USE_BUILTIN)
1077 self.feedItems.clear()
1078 for key in self.listing.getListOfFeeds():
1079 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1080 if unreadItems > 0 or not self.config.getHideReadFeeds():
1081 title = self.listing.getFeedTitle(key)
1082 updateTime = self.listing.getFeedUpdateTime(key)
1084 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1087 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1089 markup = FEED_TEMPLATE % (title, subtitle)
1092 icon_filename = self.listing.getFavicon(key)
1093 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1094 LIST_ICON_SIZE, LIST_ICON_SIZE)
1096 pixbuf = default_pixbuf
1098 self.feedItems.append((pixbuf, markup, key))
1100 def on_feedList_row_activated(self, treeview, path, column):
1101 model = treeview.get_model()
1102 iter = model.get_iter(path)
1103 key = model.get_value(iter, COLUMN_KEY)
1106 def openFeed(self, key):
1110 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1111 self.feed_lock = get_lock(key)
1112 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1113 self.listing.getFeedTitle(key), key, \
1114 self.config, self.updateDbusHandler)
1115 self.disp.connect("feed-closed", self.onFeedClosed)
1118 def onFeedClosed(self, object, key):
1119 #self.listing.saveConfig()
1121 gobject.idle_add(self.onFeedClosedTimeout)
1122 self.displayListing()
1123 #self.updateDbusHandler.ArticleCountUpdated()
1125 def onFeedClosedTimeout(self):
1126 self.listing.saveConfig()
1128 self.updateDbusHandler.ArticleCountUpdated()
1131 self.window.connect("destroy", gtk.main_quit)
1133 self.listing.saveConfig()
1136 def prefsClosed(self, *widget):
1138 self.orientation.set_mode(self.config.getOrientation())
1141 self.displayListing()
1142 self.checkAutoUpdate()
1144 def checkAutoUpdate(self, *widget):
1145 interval = int(self.config.getUpdateInterval()*3600000)
1146 if self.config.isAutoUpdateEnabled():
1147 if self.autoupdate == False:
1148 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1149 self.autoupdate = interval
1150 elif not self.autoupdate == interval:
1151 # If auto-update is enabled, but not at the right frequency
1152 gobject.source_remove(self.autoupdateId)
1153 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1154 self.autoupdate = interval
1156 if not self.autoupdate == False:
1157 gobject.source_remove(self.autoupdateId)
1158 self.autoupdate = False
1160 def automaticUpdate(self, *widget):
1161 # Need to check for internet connection
1162 # If no internet connection, try again in 10 minutes:
1163 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1164 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1165 #from time import localtime, strftime
1166 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1168 self.button_update_clicked(None, None)
1171 def stopUpdate(self):
1172 # Not implemented in the app (see update_feeds.py)
1174 self.downloadDialog.listOfKeys = []
1178 def getStatus(self):
1180 for key in self.listing.getListOfFeeds():
1181 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1182 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1184 status = "No unread items"
1187 if __name__ == "__main__":
1188 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1189 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1190 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1191 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1192 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1193 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1194 gobject.threads_init()
1195 if not isdir(CONFIGDIR):
1199 print "Error: Can't create configuration directory"
1200 from sys import exit