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(hildon.WizardDialog):
145 def __init__(self, parent, urlIn, titleIn=None):
147 self.notebook = gtk.Notebook()
149 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
150 self.nameEntry.set_placeholder("Enter Feed Name")
151 vbox = gtk.VBox(False,10)
152 label = gtk.Label("Enter Feed Name:")
153 vbox.pack_start(label)
154 vbox.pack_start(self.nameEntry)
155 if not titleIn == None:
156 self.nameEntry.set_text(titleIn)
157 self.notebook.append_page(vbox, None)
159 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
160 self.urlEntry.set_placeholder("Enter a URL")
161 self.urlEntry.set_text(urlIn)
162 self.urlEntry.select_region(0,-1)
164 vbox = gtk.VBox(False,10)
165 label = gtk.Label("Enter Feed URL:")
166 vbox.pack_start(label)
167 vbox.pack_start(self.urlEntry)
168 self.notebook.append_page(vbox, None)
170 labelEnd = gtk.Label("Success")
172 self.notebook.append_page(labelEnd, None)
174 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
176 # Set a handler for "switch-page" signal
177 #self.notebook.connect("switch_page", self.on_page_switch, self)
179 # Set a function to decide if user can go to next page
180 self.set_forward_page_func(self.some_page_func)
185 return (self.nameEntry.get_text(), self.urlEntry.get_text())
187 def on_page_switch(self, notebook, page, num, dialog):
190 def some_page_func(self, nb, current, userdata):
191 # Validate data for 1st page
193 return len(self.nameEntry.get_text()) != 0
195 # Check the url is not null, and starts with http
196 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
202 class Download(Thread):
203 def __init__(self, listing, key, config):
204 Thread.__init__(self)
205 self.listing = listing
210 (use_proxy, proxy) = self.config.getProxy()
211 key_lock = get_lock(self.key)
214 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
216 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
220 class DownloadBar(gtk.ProgressBar):
221 def __init__(self, parent, listing, listOfKeys, config, single=False):
223 update_lock = get_lock("update_lock")
224 if update_lock != None:
225 gtk.ProgressBar.__init__(self)
226 self.listOfKeys = listOfKeys[:]
227 self.listing = listing
228 self.total = len(self.listOfKeys)
232 (use_proxy, proxy) = self.config.getProxy()
234 opener = build_opener(proxy)
236 opener = build_opener()
238 opener.addheaders = [('User-agent', USER_AGENT)]
239 install_opener(opener)
242 # In preparation for i18n/l10n
244 return (a if n == 1 else b)
246 self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
249 self.set_fraction(self.fraction)
252 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
254 def update_progress_bar(self):
255 #self.progress_bar.pulse()
256 if activeCount() < 4:
257 x = activeCount() - 1
258 k = len(self.listOfKeys)
259 fin = self.total - k - x
260 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
261 #print x, k, fin, fraction
262 self.set_fraction(fraction)
264 if len(self.listOfKeys)>0:
265 self.current = self.current+1
266 key = self.listOfKeys.pop()
267 #if self.single == True:
268 # Check if the feed is being displayed
269 download = Download(self.listing, key, self.config)
272 elif activeCount() > 1:
275 #self.waitingWindow.destroy()
281 self.emit("download-done", "success")
286 class SortList(hildon.StackableWindow):
287 def __init__(self, parent, listing, feedingit, after_closing):
288 hildon.StackableWindow.__init__(self)
289 self.set_transient_for(parent)
290 self.set_title('Subscriptions')
291 self.listing = listing
292 self.feedingit = feedingit
293 self.after_closing = after_closing
294 self.connect('destroy', lambda w: self.after_closing())
295 self.vbox2 = gtk.VBox(False, 2)
297 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
298 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
299 button.connect("clicked", self.buttonUp)
300 self.vbox2.pack_start(button, expand=False, fill=False)
302 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
303 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
304 button.connect("clicked", self.buttonDown)
305 self.vbox2.pack_start(button, expand=False, fill=False)
307 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
309 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
310 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
311 button.connect("clicked", self.buttonAdd)
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_information', gtk.ICON_SIZE_BUTTON))
316 button.connect("clicked", self.buttonEdit)
317 self.vbox2.pack_start(button, expand=False, fill=False)
319 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
320 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
321 button.connect("clicked", self.buttonDelete)
322 self.vbox2.pack_start(button, expand=False, fill=False)
324 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
325 #button.set_label("Done")
326 #button.connect("clicked", self.buttonDone)
327 #self.vbox.pack_start(button)
328 self.hbox2= gtk.HBox(False, 10)
329 self.pannableArea = hildon.PannableArea()
330 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
331 self.treeview = gtk.TreeView(self.treestore)
332 self.hbox2.pack_start(self.pannableArea, expand=True)
334 self.hbox2.pack_end(self.vbox2, expand=False)
335 self.set_default_size(-1, 600)
338 menu = hildon.AppMenu()
339 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
340 button.set_label("Import from OPML")
341 button.connect("clicked", self.feedingit.button_import_clicked)
344 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
345 button.set_label("Export to OPML")
346 button.connect("clicked", self.feedingit.button_export_clicked)
348 self.set_app_menu(menu)
352 #self.connect("destroy", self.buttonDone)
354 def displayFeeds(self):
355 self.treeview.destroy()
356 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
357 self.treeview = gtk.TreeView()
359 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
360 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
362 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
364 self.pannableArea.add(self.treeview)
368 def refreshList(self, selected=None, offset=0):
369 #rect = self.treeview.get_visible_rect()
370 #y = rect.y+rect.height
371 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
372 for key in self.listing.getListOfFeeds():
373 item = self.treestore.append([self.listing.getFeedTitle(key), key])
376 self.treeview.set_model(self.treestore)
377 if not selected == None:
378 self.treeview.get_selection().select_iter(selectedItem)
379 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
380 self.pannableArea.show_all()
382 def getSelectedItem(self):
383 (model, iter) = self.treeview.get_selection().get_selected()
386 return model.get_value(iter, 1)
388 def findIndex(self, key):
392 for row in self.treestore:
394 return (before, row.iter)
395 if key == list(row)[0]:
399 return (before, None)
401 def buttonUp(self, button):
402 key = self.getSelectedItem()
404 self.listing.moveUp(key)
405 self.refreshList(key, -10)
407 def buttonDown(self, button):
408 key = self.getSelectedItem()
410 self.listing.moveDown(key)
411 self.refreshList(key, 10)
413 def buttonDelete(self, button):
414 key = self.getSelectedItem()
416 self.listing.removeFeed(key)
419 def buttonEdit(self, button):
420 key = self.getSelectedItem()
422 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
425 (title, url) = wizard.getData()
426 if (not title == '') and (not url == ''):
427 self.listing.editFeed(key, title, url)
431 def buttonDone(self, *args):
434 def buttonAdd(self, button, urlIn="http://"):
435 wizard = AddWidgetWizard(self, urlIn)
438 (title, url) = wizard.getData()
439 if (not title == '') and (not url == ''):
440 self.listing.addFeed(title, url)
445 class DisplayArticle(hildon.StackableWindow):
446 def __init__(self, feed, id, key, config, listing):
447 hildon.StackableWindow.__init__(self)
448 #self.imageDownloader = ImageDownloader()
453 #self.set_title(feed.getTitle(id))
454 self.set_title(self.listing.getFeedTitle(key))
456 self.set_for_removal = False
458 # Init the article display
459 #if self.config.getWebkitSupport():
460 self.view = WebView()
461 #self.view.set_editable(False)
464 # self.view = gtkhtml2.View()
465 # self.document = gtkhtml2.Document()
466 # self.view.set_document(self.document)
467 # self.document.connect("link_clicked", self._signal_link_clicked)
468 self.pannable_article = hildon.PannableArea()
469 self.pannable_article.add(self.view)
470 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
471 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
473 #if self.config.getWebkitSupport():
474 contentLink = self.feed.getContentLink(self.id)
475 self.feed.setEntryRead(self.id)
476 #if key=="ArchivedArticles":
477 if contentLink.startswith("/home/user/"):
478 self.view.open("file://" + contentLink)
480 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
481 self.view.connect("motion-notify-event", lambda w,ev: True)
482 self.view.connect('load-started', self.load_started)
483 self.view.connect('load-finished', self.load_finished)
486 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
487 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
489 # if not key == "ArchivedArticles":
490 # Do not download images if the feed is "Archived Articles"
491 # self.document.connect("request-url", self._signal_request_url)
493 # self.document.clear()
494 # self.document.open_stream("text/html")
495 # self.document.write_stream(self.text)
496 # self.document.close_stream()
498 menu = hildon.AppMenu()
499 # Create a button and add it to the menu
500 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
501 button.set_label("Allow horizontal scrolling")
502 button.connect("clicked", self.horiz_scrolling_button)
505 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
506 button.set_label("Open in browser")
507 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
510 if key == "ArchivedArticles":
511 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
512 button.set_label("Remove from archived articles")
513 button.connect("clicked", self.remove_archive_button)
515 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
516 button.set_label("Add to archived articles")
517 button.connect("clicked", self.archive_button)
520 self.set_app_menu(menu)
523 #self.event_box = gtk.EventBox()
524 #self.event_box.add(self.pannable_article)
525 self.add(self.pannable_article)
528 self.pannable_article.show_all()
530 self.destroyId = self.connect("destroy", self.destroyWindow)
532 self.view.connect("button_press_event", self.button_pressed)
533 self.gestureId = self.view.connect("button_release_event", self.button_released)
534 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
536 def load_started(self, *widget):
537 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
539 def load_finished(self, *widget):
540 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
542 def button_pressed(self, window, event):
543 #print event.x, event.y
544 self.coords = (event.x, event.y)
546 def button_released(self, window, event):
547 x = self.coords[0] - event.x
548 y = self.coords[1] - event.y
550 if (2*abs(y) < abs(x)):
552 self.emit("article-previous", self.id)
554 self.emit("article-next", self.id)
558 #def gesture(self, widget, direction, startx, starty):
559 # if (direction == 3):
560 # self.emit("article-next", self.index)
561 # if (direction == 2):
562 # self.emit("article-previous", self.index)
563 #print startx, starty
564 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
566 def destroyWindow(self, *args):
567 self.disconnect(self.destroyId)
568 if self.set_for_removal:
569 self.emit("article-deleted", self.id)
571 self.emit("article-closed", self.id)
572 #self.imageDownloader.stopAll()
575 def horiz_scrolling_button(self, *widget):
576 self.pannable_article.disconnect(self.gestureId)
577 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
579 def archive_button(self, *widget):
580 # Call the listing.addArchivedArticle
581 self.listing.addArchivedArticle(self.key, self.id)
583 def remove_archive_button(self, *widget):
584 self.set_for_removal = True
586 #def reloadArticle(self, *widget):
587 # if threading.activeCount() > 1:
588 # Image thread are still running, come back in a bit
591 # for (stream, imageThread) in self.images:
593 # stream.write(imageThread.data)
598 def _signal_link_clicked(self, object, link):
600 bus = dbus.SessionBus()
601 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
602 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
603 iface.open_new_window(link)
605 #def _signal_request_url(self, object, url, stream):
607 # self.imageDownloader.queueImage(url, stream)
608 #imageThread = GetImage(url)
610 #self.images.append((stream, imageThread))
613 class DisplayFeed(hildon.StackableWindow):
614 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
615 hildon.StackableWindow.__init__(self)
616 self.listing = listing
618 self.feedTitle = title
619 self.set_title(title)
622 self.updateDbusHandler = updateDbusHandler
624 self.downloadDialog = False
626 #self.listing.setCurrentlyDisplayedFeed(self.key)
630 menu = hildon.AppMenu()
631 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
632 button.set_label("Update feed")
633 button.connect("clicked", self.button_update_clicked)
636 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
637 button.set_label("Mark all as read")
638 button.connect("clicked", self.buttonReadAllClicked)
641 if key=="ArchivedArticles":
642 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
643 button.set_label("Delete read articles")
644 button.connect("clicked", self.buttonPurgeArticles)
647 self.set_app_menu(menu)
652 self.connect('configure-event', self.on_configure_event)
653 self.connect("destroy", self.destroyWindow)
655 def on_configure_event(self, window, event):
656 if getattr(self, 'markup_renderer', None) is None:
659 # Fix up the column width for wrapping the text when the window is
660 # resized (i.e. orientation changed)
661 self.markup_renderer.set_property('wrap-width', event.width-20)
662 it = self.feedItems.get_iter_first()
663 while it is not None:
664 markup = self.feedItems.get_value(it, FEED_COLUMN_MARKUP)
665 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
666 it = self.feedItems.iter_next(it)
668 def destroyWindow(self, *args):
669 #self.feed.saveUnread(CONFIGDIR)
670 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
671 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
672 self.emit("feed-closed", self.key)
674 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
675 #self.listing.closeCurrentlyDisplayedFeed()
677 def fix_title(self, title):
678 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
680 def displayFeed(self):
681 self.pannableFeed = hildon.PannableArea()
683 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
685 self.feedItems = gtk.ListStore(str, str)
686 #self.feedList = gtk.TreeView(self.feedItems)
687 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
688 selection = self.feedList.get_selection()
689 selection.set_mode(gtk.SELECTION_NONE)
690 #selection.connect("changed", lambda w: True)
692 self.feedList.set_model(self.feedItems)
693 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
696 self.feedList.set_hover_selection(False)
697 #self.feedList.set_property('enable-grid-lines', True)
698 #self.feedList.set_property('hildon-mode', 1)
699 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
701 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
703 vbox= gtk.VBox(False, 10)
704 vbox.pack_start(self.feedList)
706 self.pannableFeed.add_with_viewport(vbox)
708 self.markup_renderer = gtk.CellRendererText()
709 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
710 self.markup_renderer.set_property('background', "#333333")
711 (width, height) = self.get_size()
712 self.markup_renderer.set_property('wrap-width', width-20)
713 self.markup_renderer.set_property('ypad', 5)
714 self.markup_renderer.set_property('xpad', 5)
715 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
716 markup=FEED_COLUMN_MARKUP)
717 self.feedList.append_column(markup_column)
719 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
720 hideReadArticles = self.config.getHideReadArticles()
722 for id in self.feed.getIds():
725 isRead = self.feed.isEntryRead(id)
728 if not ( isRead and hideReadArticles ):
729 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
730 #title = self.feed.getTitle(id)
731 title = self.fix_title(self.feed.getTitle(id))
733 #if self.feed.isEntryRead(id):
735 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
737 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
739 self.feedItems.append((markup, id))
742 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
744 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
745 self.feedItems.append((markup, ""))
747 self.add(self.pannableFeed)
751 self.pannableFeed.destroy()
752 #self.remove(self.pannableFeed)
754 def on_feedList_row_activated(self, treeview, path): #, column):
755 selection = self.feedList.get_selection()
756 selection.set_mode(gtk.SELECTION_SINGLE)
757 self.feedList.get_selection().select_path(path)
758 model = treeview.get_model()
759 iter = model.get_iter(path)
760 key = model.get_value(iter, FEED_COLUMN_KEY)
761 # Emulate legacy "button_clicked" call via treeview
762 gobject.idle_add(self.button_clicked, treeview, key)
765 def button_clicked(self, button, index, previous=False, next=False):
766 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
767 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
768 stack = hildon.WindowStack.get_default()
771 stack.pop_and_push(1, newDisp, tmp)
773 gobject.timeout_add(200, self.destroyArticle, tmp)
778 if type(self.disp).__name__ == "DisplayArticle":
779 gobject.timeout_add(200, self.destroyArticle, self.disp)
786 if self.key == "ArchivedArticles":
787 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
788 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
789 self.ids.append(self.disp.connect("article-next", self.nextArticle))
790 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
792 def buttonPurgeArticles(self, *widget):
794 self.feed.purgeReadArticles()
795 self.feed.saveUnread(CONFIGDIR)
796 self.feed.saveFeed(CONFIGDIR)
799 def destroyArticle(self, handle):
800 handle.destroyWindow()
802 def mark_item_read(self, key):
803 it = self.feedItems.get_iter_first()
804 while it is not None:
805 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
807 title = self.fix_title(self.feed.getTitle(key))
808 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
809 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
811 it = self.feedItems.iter_next(it)
813 def nextArticle(self, object, index):
814 self.mark_item_read(index)
815 id = self.feed.getNextId(index)
816 if self.config.getHideReadArticles():
819 isRead = self.feed.isEntryRead(id)
822 while isRead and id != index:
823 id = self.feed.getNextId(id)
826 isRead = self.feed.isEntryRead(id)
830 self.button_clicked(object, id, next=True)
832 def previousArticle(self, object, index):
833 self.mark_item_read(index)
834 id = self.feed.getPreviousId(index)
835 if self.config.getHideReadArticles():
838 isRead = self.feed.isEntryRead(id)
841 while isRead and id != index:
842 id = self.feed.getPreviousId(id)
845 isRead = self.feed.isEntryRead(id)
849 self.button_clicked(object, id, previous=True)
851 def onArticleClosed(self, object, index):
852 selection = self.feedList.get_selection()
853 selection.set_mode(gtk.SELECTION_NONE)
854 self.mark_item_read(index)
856 def onArticleDeleted(self, object, index):
858 self.feed.removeArticle(index)
859 self.feed.saveUnread(CONFIGDIR)
860 self.feed.saveFeed(CONFIGDIR)
863 def button_update_clicked(self, button):
864 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
865 if not type(self.downloadDialog).__name__=="DownloadBar":
866 self.pannableFeed.destroy()
867 self.vbox = gtk.VBox(False, 10)
868 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
869 self.downloadDialog.connect("download-done", self.onDownloadsDone)
870 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
874 def onDownloadsDone(self, *widget):
876 self.feed = self.listing.getFeed(self.key)
878 self.updateDbusHandler.ArticleCountUpdated()
880 def buttonReadAllClicked(self, button):
881 for index in self.feed.getIds():
882 self.feed.setEntryRead(index)
883 self.mark_item_read(index)
889 self.window = hildon.StackableWindow()
890 self.window.set_title(__appname__)
891 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
892 self.mainVbox = gtk.VBox(False,10)
894 self.introLabel = gtk.Label("Loading...")
897 self.mainVbox.pack_start(self.introLabel)
899 self.window.add(self.mainVbox)
900 self.window.show_all()
901 self.config = Config(self.window, CONFIGDIR+"config.ini")
902 gobject.idle_add(self.createWindow)
904 def createWindow(self):
905 self.app_lock = get_lock("app_lock")
906 if self.app_lock == None:
907 self.introLabel.set_label("Update in progress, please wait.")
908 gobject.timeout_add_seconds(3, self.createWindow)
910 self.listing = Listing(CONFIGDIR)
912 self.downloadDialog = False
914 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
915 self.orientation.set_mode(self.config.getOrientation())
917 print "Could not start rotation manager"
919 menu = hildon.AppMenu()
920 # Create a button and add it to the menu
921 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
922 button.set_label("Update feeds")
923 button.connect("clicked", self.button_update_clicked, "All")
926 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
927 button.set_label("Mark all as read")
928 button.connect("clicked", self.button_markAll)
931 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
932 button.set_label("Manage subscriptions")
933 button.connect("clicked", self.button_organize_clicked)
936 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
937 button.set_label("Settings")
938 button.connect("clicked", self.button_preferences_clicked)
941 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
942 button.set_label("About")
943 button.connect("clicked", self.button_about_clicked)
946 self.window.set_app_menu(menu)
949 #self.feedWindow = hildon.StackableWindow()
950 #self.articleWindow = hildon.StackableWindow()
951 self.introLabel.destroy()
952 self.pannableListing = hildon.PannableArea()
953 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
954 self.feedList = gtk.TreeView(self.feedItems)
955 self.feedList.connect('row-activated', self.on_feedList_row_activated)
956 self.pannableListing.add(self.feedList)
958 icon_renderer = gtk.CellRendererPixbuf()
959 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
960 icon_column = gtk.TreeViewColumn('', icon_renderer, \
962 self.feedList.append_column(icon_column)
964 markup_renderer = gtk.CellRendererText()
965 markup_column = gtk.TreeViewColumn('', markup_renderer, \
966 markup=COLUMN_MARKUP)
967 self.feedList.append_column(markup_column)
968 self.mainVbox.pack_start(self.pannableListing)
969 self.mainVbox.show_all()
971 self.displayListing()
972 self.autoupdate = False
973 self.checkAutoUpdate()
974 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
975 gobject.idle_add(self.enableDbus)
977 def enableDbus(self):
978 self.dbusHandler = ServerObject(self)
979 self.updateDbusHandler = UpdateServerObject(self)
981 def button_markAll(self, button):
982 for key in self.listing.getListOfFeeds():
983 feed = self.listing.getFeed(key)
984 for id in feed.getIds():
985 feed.setEntryRead(id)
986 feed.saveUnread(CONFIGDIR)
987 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
988 self.displayListing()
990 def button_about_clicked(self, button):
991 HeAboutDialog.present(self.window, \
1001 def button_export_clicked(self, button):
1002 opml = ExportOpmlData(self.window, self.listing)
1004 def button_import_clicked(self, button):
1005 opml = GetOpmlData(self.window)
1006 feeds = opml.getData()
1007 for (title, url) in feeds:
1008 self.listing.addFeed(title, url)
1009 self.displayListing()
1011 def addFeed(self, urlIn="http://"):
1012 wizard = AddWidgetWizard(self.window, urlIn)
1015 (title, url) = wizard.getData()
1016 if (not title == '') and (not url == ''):
1017 self.listing.addFeed(title, url)
1019 self.displayListing()
1021 def button_organize_clicked(self, button):
1022 def after_closing():
1023 self.listing.saveConfig()
1024 self.displayListing()
1025 SortList(self.window, self.listing, self, after_closing)
1027 def button_update_clicked(self, button, key):
1028 if not type(self.downloadDialog).__name__=="DownloadBar":
1029 self.updateDbusHandler.UpdateStarted()
1030 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1031 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1032 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1033 self.mainVbox.show_all()
1034 #self.displayListing()
1036 def onDownloadsDone(self, *widget):
1037 self.downloadDialog.destroy()
1038 self.downloadDialog = False
1039 self.displayListing()
1040 self.updateDbusHandler.UpdateFinished()
1041 self.updateDbusHandler.ArticleCountUpdated()
1043 def button_preferences_clicked(self, button):
1044 dialog = self.config.createDialog()
1045 dialog.connect("destroy", self.prefsClosed)
1047 def show_confirmation_note(self, parent, title):
1048 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1050 retcode = gtk.Dialog.run(note)
1053 if retcode == gtk.RESPONSE_OK:
1058 def displayListing(self):
1059 icon_theme = gtk.icon_theme_get_default()
1060 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1061 gtk.ICON_LOOKUP_USE_BUILTIN)
1063 self.feedItems.clear()
1064 for key in self.listing.getListOfFeeds():
1065 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1066 if unreadItems > 0 or not self.config.getHideReadFeeds():
1067 title = self.listing.getFeedTitle(key)
1068 updateTime = self.listing.getFeedUpdateTime(key)
1070 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1073 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1075 markup = FEED_TEMPLATE % (title, subtitle)
1078 icon_filename = self.listing.getFavicon(key)
1079 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1080 LIST_ICON_SIZE, LIST_ICON_SIZE)
1082 pixbuf = default_pixbuf
1084 self.feedItems.append((pixbuf, markup, key))
1086 def on_feedList_row_activated(self, treeview, path, column):
1087 model = treeview.get_model()
1088 iter = model.get_iter(path)
1089 key = model.get_value(iter, COLUMN_KEY)
1092 def openFeed(self, key):
1096 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1097 self.feed_lock = get_lock(key)
1098 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1099 self.listing.getFeedTitle(key), key, \
1100 self.config, self.updateDbusHandler)
1101 self.disp.connect("feed-closed", self.onFeedClosed)
1104 def onFeedClosed(self, object, key):
1105 #self.listing.saveConfig()
1107 gobject.idle_add(self.onFeedClosedTimeout)
1108 self.displayListing()
1109 #self.updateDbusHandler.ArticleCountUpdated()
1111 def onFeedClosedTimeout(self):
1112 self.listing.saveConfig()
1114 self.updateDbusHandler.ArticleCountUpdated()
1117 self.window.connect("destroy", gtk.main_quit)
1119 self.listing.saveConfig()
1122 def prefsClosed(self, *widget):
1124 self.orientation.set_mode(self.config.getOrientation())
1127 self.displayListing()
1128 self.checkAutoUpdate()
1130 def checkAutoUpdate(self, *widget):
1131 interval = int(self.config.getUpdateInterval()*3600000)
1132 if self.config.isAutoUpdateEnabled():
1133 if self.autoupdate == False:
1134 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1135 self.autoupdate = interval
1136 elif not self.autoupdate == interval:
1137 # If auto-update is enabled, but not at the right frequency
1138 gobject.source_remove(self.autoupdateId)
1139 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1140 self.autoupdate = interval
1142 if not self.autoupdate == False:
1143 gobject.source_remove(self.autoupdateId)
1144 self.autoupdate = False
1146 def automaticUpdate(self, *widget):
1147 # Need to check for internet connection
1148 # If no internet connection, try again in 10 minutes:
1149 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1150 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1151 #from time import localtime, strftime
1152 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1154 self.button_update_clicked(None, None)
1157 def stopUpdate(self):
1158 # Not implemented in the app (see update_feeds.py)
1160 self.downloadDialog.listOfKeys = []
1164 def getStatus(self):
1166 for key in self.listing.getListOfFeeds():
1167 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1168 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1170 status = "No unread items"
1173 if __name__ == "__main__":
1174 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1175 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1176 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1177 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1178 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1179 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1180 gobject.threads_init()
1181 if not isdir(CONFIGDIR):
1185 print "Error: Can't create configuration directory"
1186 from sys import exit