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(gtk.Dialog):
282 def __init__(self, parent, listing):
283 gtk.Dialog.__init__(self, "Organizer", parent)
284 self.listing = listing
286 self.vbox2 = gtk.VBox(False, 10)
288 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
289 button.set_label("Move Up")
290 button.connect("clicked", self.buttonUp)
291 self.vbox2.pack_start(button, expand=False, fill=False)
293 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
294 button.set_label("Move Down")
295 button.connect("clicked", self.buttonDown)
296 self.vbox2.pack_start(button, expand=False, fill=False)
298 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
299 button.set_label("Add Feed")
300 button.connect("clicked", self.buttonAdd)
301 self.vbox2.pack_start(button, expand=False, fill=False)
303 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
304 button.set_label("Edit Feed")
305 button.connect("clicked", self.buttonEdit)
306 self.vbox2.pack_start(button, expand=False, fill=False)
308 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
309 button.set_label("Delete")
310 button.connect("clicked", self.buttonDelete)
311 self.vbox2.pack_start(button, expand=False, fill=False)
313 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
314 #button.set_label("Done")
315 #button.connect("clicked", self.buttonDone)
316 #self.vbox.pack_start(button)
317 self.hbox2= gtk.HBox(False, 10)
318 self.pannableArea = hildon.PannableArea()
319 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
320 self.treeview = gtk.TreeView(self.treestore)
321 self.hbox2.pack_start(self.pannableArea, expand=True)
323 self.hbox2.pack_end(self.vbox2, expand=False)
324 self.set_default_size(-1, 600)
325 self.vbox.pack_start(self.hbox2)
328 #self.connect("destroy", self.buttonDone)
330 def displayFeeds(self):
331 self.treeview.destroy()
332 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
333 self.treeview = gtk.TreeView()
335 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
336 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
338 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
340 self.pannableArea.add(self.treeview)
344 def refreshList(self, selected=None, offset=0):
345 #rect = self.treeview.get_visible_rect()
346 #y = rect.y+rect.height
347 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
348 for key in self.listing.getListOfFeeds():
349 item = self.treestore.append([self.listing.getFeedTitle(key), key])
352 self.treeview.set_model(self.treestore)
353 if not selected == None:
354 self.treeview.get_selection().select_iter(selectedItem)
355 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
356 self.pannableArea.show_all()
358 def getSelectedItem(self):
359 (model, iter) = self.treeview.get_selection().get_selected()
362 return model.get_value(iter, 1)
364 def findIndex(self, key):
368 for row in self.treestore:
370 return (before, row.iter)
371 if key == list(row)[0]:
375 return (before, None)
377 def buttonUp(self, button):
378 key = self.getSelectedItem()
380 self.listing.moveUp(key)
381 self.refreshList(key, -10)
383 def buttonDown(self, button):
384 key = self.getSelectedItem()
386 self.listing.moveDown(key)
387 self.refreshList(key, 10)
389 def buttonDelete(self, button):
390 key = self.getSelectedItem()
392 self.listing.removeFeed(key)
395 def buttonEdit(self, button):
396 key = self.getSelectedItem()
398 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
401 (title, url) = wizard.getData()
402 if (not title == '') and (not url == ''):
403 self.listing.editFeed(key, title, url)
407 def buttonDone(self, *args):
410 def buttonAdd(self, button, urlIn="http://"):
411 wizard = AddWidgetWizard(self, urlIn)
414 (title, url) = wizard.getData()
415 if (not title == '') and (not url == ''):
416 self.listing.addFeed(title, url)
421 class DisplayArticle(hildon.StackableWindow):
422 def __init__(self, feed, id, key, config, listing):
423 hildon.StackableWindow.__init__(self)
424 #self.imageDownloader = ImageDownloader()
429 #self.set_title(feed.getTitle(id))
430 self.set_title(self.listing.getFeedTitle(key))
432 self.set_for_removal = False
434 # Init the article display
435 #if self.config.getWebkitSupport():
436 self.view = WebView()
437 #self.view.set_editable(False)
440 # self.view = gtkhtml2.View()
441 # self.document = gtkhtml2.Document()
442 # self.view.set_document(self.document)
443 # self.document.connect("link_clicked", self._signal_link_clicked)
444 self.pannable_article = hildon.PannableArea()
445 self.pannable_article.add(self.view)
446 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
447 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
449 #if self.config.getWebkitSupport():
450 contentLink = self.feed.getContentLink(self.id)
451 self.feed.setEntryRead(self.id)
452 #if key=="ArchivedArticles":
453 if contentLink.startswith("/home/user/"):
454 self.view.open("file://" + contentLink)
456 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
457 self.view.connect("motion-notify-event", lambda w,ev: True)
458 self.view.connect('load-started', self.load_started)
459 self.view.connect('load-finished', self.load_finished)
462 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
463 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
465 # if not key == "ArchivedArticles":
466 # Do not download images if the feed is "Archived Articles"
467 # self.document.connect("request-url", self._signal_request_url)
469 # self.document.clear()
470 # self.document.open_stream("text/html")
471 # self.document.write_stream(self.text)
472 # self.document.close_stream()
474 menu = hildon.AppMenu()
475 # Create a button and add it to the menu
476 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
477 button.set_label("Allow Horizontal Scrolling")
478 button.connect("clicked", self.horiz_scrolling_button)
481 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
482 button.set_label("Open in Browser")
483 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
486 if key == "ArchivedArticles":
487 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
488 button.set_label("Remove from Archived Articles")
489 button.connect("clicked", self.remove_archive_button)
491 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
492 button.set_label("Add to Archived Articles")
493 button.connect("clicked", self.archive_button)
496 self.set_app_menu(menu)
499 #self.event_box = gtk.EventBox()
500 #self.event_box.add(self.pannable_article)
501 self.add(self.pannable_article)
504 self.pannable_article.show_all()
506 self.destroyId = self.connect("destroy", self.destroyWindow)
508 self.view.connect("button_press_event", self.button_pressed)
509 self.gestureId = self.view.connect("button_release_event", self.button_released)
510 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
512 def load_started(self, *widget):
513 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
515 def load_finished(self, *widget):
516 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
518 def button_pressed(self, window, event):
519 #print event.x, event.y
520 self.coords = (event.x, event.y)
522 def button_released(self, window, event):
523 x = self.coords[0] - event.x
524 y = self.coords[1] - event.y
526 if (2*abs(y) < abs(x)):
528 self.emit("article-previous", self.id)
530 self.emit("article-next", self.id)
534 #def gesture(self, widget, direction, startx, starty):
535 # if (direction == 3):
536 # self.emit("article-next", self.index)
537 # if (direction == 2):
538 # self.emit("article-previous", self.index)
539 #print startx, starty
540 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
542 def destroyWindow(self, *args):
543 self.disconnect(self.destroyId)
544 if self.set_for_removal:
545 self.emit("article-deleted", self.id)
547 self.emit("article-closed", self.id)
548 #self.imageDownloader.stopAll()
551 def horiz_scrolling_button(self, *widget):
552 self.pannable_article.disconnect(self.gestureId)
553 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
555 def archive_button(self, *widget):
556 # Call the listing.addArchivedArticle
557 self.listing.addArchivedArticle(self.key, self.id)
559 def remove_archive_button(self, *widget):
560 self.set_for_removal = True
562 #def reloadArticle(self, *widget):
563 # if threading.activeCount() > 1:
564 # Image thread are still running, come back in a bit
567 # for (stream, imageThread) in self.images:
569 # stream.write(imageThread.data)
574 def _signal_link_clicked(self, object, link):
576 bus = dbus.SessionBus()
577 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
578 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
579 iface.open_new_window(link)
581 #def _signal_request_url(self, object, url, stream):
583 # self.imageDownloader.queueImage(url, stream)
584 #imageThread = GetImage(url)
586 #self.images.append((stream, imageThread))
589 class DisplayFeed(hildon.StackableWindow):
590 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
591 hildon.StackableWindow.__init__(self)
592 self.listing = listing
594 self.feedTitle = title
595 self.set_title(title)
598 self.updateDbusHandler = updateDbusHandler
600 self.downloadDialog = False
602 #self.listing.setCurrentlyDisplayedFeed(self.key)
606 menu = hildon.AppMenu()
607 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
608 button.set_label("Update Feed")
609 button.connect("clicked", self.button_update_clicked)
612 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
613 button.set_label("Mark All As Read")
614 button.connect("clicked", self.buttonReadAllClicked)
617 if key=="ArchivedArticles":
618 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
619 button.set_label("Purge Read Articles")
620 button.connect("clicked", self.buttonPurgeArticles)
623 self.set_app_menu(menu)
628 self.connect('configure-event', self.on_configure_event)
629 self.connect("destroy", self.destroyWindow)
631 def on_configure_event(self, window, event):
632 if getattr(self, 'markup_renderer', None) is None:
635 # Fix up the column width for wrapping the text when the window is
636 # resized (i.e. orientation changed)
637 self.markup_renderer.set_property('wrap-width', event.width-10)
639 def destroyWindow(self, *args):
640 #self.feed.saveUnread(CONFIGDIR)
641 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
642 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
643 self.emit("feed-closed", self.key)
645 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
646 #self.listing.closeCurrentlyDisplayedFeed()
648 def fix_title(self, title):
649 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
651 def displayFeed(self):
652 self.pannableFeed = hildon.PannableArea()
654 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
656 self.feedItems = gtk.ListStore(str, str)
657 #self.feedList = gtk.TreeView(self.feedItems)
658 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
659 selection = self.feedList.get_selection()
660 selection.set_mode(gtk.SELECTION_NONE)
661 #selection.connect("changed", lambda w: True)
663 self.feedList.set_model(self.feedItems)
664 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
667 self.feedList.set_hover_selection(False)
668 #self.feedList.set_property('enable-grid-lines', True)
669 #self.feedList.set_property('hildon-mode', 1)
670 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
672 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
674 vbox= gtk.VBox(False, 10)
675 vbox.pack_start(self.feedList)
677 self.pannableFeed.add_with_viewport(vbox)
679 self.markup_renderer = gtk.CellRendererText()
680 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
681 self.markup_renderer.set_property('wrap-width', 780)
682 self.markup_renderer.set_property('ypad', 5)
683 self.markup_renderer.set_property('xpad', 5)
684 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
685 markup=FEED_COLUMN_MARKUP)
686 self.feedList.append_column(markup_column)
688 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
689 hideReadArticles = self.config.getHideReadArticles()
691 for id in self.feed.getIds():
694 isRead = self.feed.isEntryRead(id)
697 if not ( isRead and hideReadArticles ):
698 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
699 #title = self.feed.getTitle(id)
700 title = self.fix_title(self.feed.getTitle(id))
702 #if self.feed.isEntryRead(id):
704 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
706 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
708 self.feedItems.append((markup, id))
711 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
713 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
714 self.feedItems.append((markup, ""))
716 self.add(self.pannableFeed)
720 self.pannableFeed.destroy()
721 #self.remove(self.pannableFeed)
723 def on_feedList_row_activated(self, treeview, path): #, column):
724 selection = self.feedList.get_selection()
725 selection.set_mode(gtk.SELECTION_SINGLE)
726 self.feedList.get_selection().select_path(path)
727 model = treeview.get_model()
728 iter = model.get_iter(path)
729 key = model.get_value(iter, FEED_COLUMN_KEY)
730 # Emulate legacy "button_clicked" call via treeview
731 gobject.idle_add(self.button_clicked, treeview, key)
734 def button_clicked(self, button, index, previous=False, next=False):
735 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
736 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
737 stack = hildon.WindowStack.get_default()
740 stack.pop_and_push(1, newDisp, tmp)
742 gobject.timeout_add(200, self.destroyArticle, tmp)
747 if type(self.disp).__name__ == "DisplayArticle":
748 gobject.timeout_add(200, self.destroyArticle, self.disp)
755 if self.key == "ArchivedArticles":
756 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
757 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
758 self.ids.append(self.disp.connect("article-next", self.nextArticle))
759 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
761 def buttonPurgeArticles(self, *widget):
763 self.feed.purgeReadArticles()
764 self.feed.saveUnread(CONFIGDIR)
765 self.feed.saveFeed(CONFIGDIR)
768 def destroyArticle(self, handle):
769 handle.destroyWindow()
771 def mark_item_read(self, key):
772 it = self.feedItems.get_iter_first()
773 while it is not None:
774 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
776 title = self.fix_title(self.feed.getTitle(key))
777 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
778 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
780 it = self.feedItems.iter_next(it)
782 def nextArticle(self, object, index):
783 self.mark_item_read(index)
784 id = self.feed.getNextId(index)
785 if self.config.getHideReadArticles():
788 isRead = self.feed.isEntryRead(id)
791 while isRead and id != index:
792 id = self.feed.getNextId(id)
795 isRead = self.feed.isEntryRead(id)
799 self.button_clicked(object, id, next=True)
801 def previousArticle(self, object, index):
802 self.mark_item_read(index)
803 id = self.feed.getPreviousId(index)
804 if self.config.getHideReadArticles():
807 isRead = self.feed.isEntryRead(id)
810 while isRead and id != index:
811 id = self.feed.getPreviousId(id)
814 isRead = self.feed.isEntryRead(id)
818 self.button_clicked(object, id, previous=True)
820 def onArticleClosed(self, object, index):
821 selection = self.feedList.get_selection()
822 selection.set_mode(gtk.SELECTION_NONE)
823 self.mark_item_read(index)
825 def onArticleDeleted(self, object, index):
827 self.feed.removeArticle(index)
828 self.feed.saveUnread(CONFIGDIR)
829 self.feed.saveFeed(CONFIGDIR)
832 def button_update_clicked(self, button):
833 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
834 if not type(self.downloadDialog).__name__=="DownloadBar":
835 self.pannableFeed.destroy()
836 self.vbox = gtk.VBox(False, 10)
837 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
838 self.downloadDialog.connect("download-done", self.onDownloadsDone)
839 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
843 def onDownloadsDone(self, *widget):
845 self.feed = self.listing.getFeed(self.key)
847 self.updateDbusHandler.ArticleCountUpdated()
849 def buttonReadAllClicked(self, button):
850 for index in self.feed.getIds():
851 self.feed.setEntryRead(index)
852 self.mark_item_read(index)
858 self.window = hildon.StackableWindow()
859 self.window.set_title(__appname__)
860 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
861 self.mainVbox = gtk.VBox(False,10)
863 self.introLabel = gtk.Label("Loading...")
866 self.mainVbox.pack_start(self.introLabel)
868 self.window.add(self.mainVbox)
869 self.window.show_all()
870 self.config = Config(self.window, CONFIGDIR+"config.ini")
871 gobject.idle_add(self.createWindow)
873 def createWindow(self):
874 self.app_lock = get_lock("app_lock")
875 if self.app_lock == None:
876 self.introLabel.set_label("Update in progress, please wait.")
877 gobject.timeout_add_seconds(3, self.createWindow)
879 self.listing = Listing(CONFIGDIR)
881 self.downloadDialog = False
883 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
884 self.orientation.set_mode(self.config.getOrientation())
886 print "Could not start rotation manager"
888 menu = hildon.AppMenu()
889 # Create a button and add it to the menu
890 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
891 button.set_label("Update All Feeds")
892 button.connect("clicked", self.button_update_clicked, "All")
895 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
896 button.set_label("Mark All As Read")
897 button.connect("clicked", self.button_markAll)
900 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
901 button.set_label("Organize Feeds")
902 button.connect("clicked", self.button_organize_clicked)
905 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
906 button.set_label("Preferences")
907 button.connect("clicked", self.button_preferences_clicked)
910 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
911 button.set_label("Import Feeds")
912 button.connect("clicked", self.button_import_clicked)
915 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
916 button.set_label("Export Feeds")
917 button.connect("clicked", self.button_export_clicked)
920 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
921 button.set_label("About")
922 button.connect("clicked", self.button_about_clicked)
925 self.window.set_app_menu(menu)
928 #self.feedWindow = hildon.StackableWindow()
929 #self.articleWindow = hildon.StackableWindow()
930 self.introLabel.destroy()
931 self.pannableListing = hildon.PannableArea()
932 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
933 self.feedList = gtk.TreeView(self.feedItems)
934 self.feedList.connect('row-activated', self.on_feedList_row_activated)
935 self.pannableListing.add(self.feedList)
937 icon_renderer = gtk.CellRendererPixbuf()
938 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
939 icon_column = gtk.TreeViewColumn('', icon_renderer, \
941 self.feedList.append_column(icon_column)
943 markup_renderer = gtk.CellRendererText()
944 markup_column = gtk.TreeViewColumn('', markup_renderer, \
945 markup=COLUMN_MARKUP)
946 self.feedList.append_column(markup_column)
947 self.mainVbox.pack_start(self.pannableListing)
948 self.mainVbox.show_all()
950 self.displayListing()
951 self.autoupdate = False
952 self.checkAutoUpdate()
953 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
954 gobject.idle_add(self.enableDbus)
956 def enableDbus(self):
957 self.dbusHandler = ServerObject(self)
958 self.updateDbusHandler = UpdateServerObject(self)
960 def button_markAll(self, button):
961 for key in self.listing.getListOfFeeds():
962 feed = self.listing.getFeed(key)
963 for id in feed.getIds():
964 feed.setEntryRead(id)
965 feed.saveUnread(CONFIGDIR)
966 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
967 self.displayListing()
969 def button_about_clicked(self, button):
970 HeAboutDialog.present(self.window, \
980 def button_export_clicked(self, button):
981 opml = ExportOpmlData(self.window, self.listing)
983 def button_import_clicked(self, button):
984 opml = GetOpmlData(self.window)
985 feeds = opml.getData()
986 for (title, url) in feeds:
987 self.listing.addFeed(title, url)
988 self.displayListing()
990 def addFeed(self, urlIn="http://"):
991 wizard = AddWidgetWizard(self.window, urlIn)
994 (title, url) = wizard.getData()
995 if (not title == '') and (not url == ''):
996 self.listing.addFeed(title, url)
998 self.displayListing()
1000 def button_organize_clicked(self, button):
1001 org = SortList(self.window, self.listing)
1004 self.listing.saveConfig()
1005 self.displayListing()
1007 def button_update_clicked(self, button, key):
1008 if not type(self.downloadDialog).__name__=="DownloadBar":
1009 self.updateDbusHandler.UpdateStarted()
1010 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1011 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1012 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1013 self.mainVbox.show_all()
1014 #self.displayListing()
1016 def onDownloadsDone(self, *widget):
1017 self.downloadDialog.destroy()
1018 self.downloadDialog = False
1019 self.displayListing()
1020 self.updateDbusHandler.UpdateFinished()
1021 self.updateDbusHandler.ArticleCountUpdated()
1023 def button_preferences_clicked(self, button):
1024 dialog = self.config.createDialog()
1025 dialog.connect("destroy", self.prefsClosed)
1027 def show_confirmation_note(self, parent, title):
1028 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1030 retcode = gtk.Dialog.run(note)
1033 if retcode == gtk.RESPONSE_OK:
1038 def displayListing(self):
1039 icon_theme = gtk.icon_theme_get_default()
1040 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1041 gtk.ICON_LOOKUP_USE_BUILTIN)
1043 self.feedItems.clear()
1044 for key in self.listing.getListOfFeeds():
1045 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1046 if unreadItems > 0 or not self.config.getHideReadFeeds():
1047 title = self.listing.getFeedTitle(key)
1048 updateTime = self.listing.getFeedUpdateTime(key)
1050 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1053 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1055 markup = FEED_TEMPLATE % (title, subtitle)
1058 icon_filename = self.listing.getFavicon(key)
1059 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1060 LIST_ICON_SIZE, LIST_ICON_SIZE)
1062 pixbuf = default_pixbuf
1064 self.feedItems.append((pixbuf, markup, key))
1066 def on_feedList_row_activated(self, treeview, path, column):
1067 model = treeview.get_model()
1068 iter = model.get_iter(path)
1069 key = model.get_value(iter, COLUMN_KEY)
1072 def openFeed(self, key):
1076 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1077 self.feed_lock = get_lock(key)
1078 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1079 self.listing.getFeedTitle(key), key, \
1080 self.config, self.updateDbusHandler)
1081 self.disp.connect("feed-closed", self.onFeedClosed)
1084 def onFeedClosed(self, object, key):
1085 #self.listing.saveConfig()
1087 gobject.idle_add(self.onFeedClosedTimeout)
1088 self.displayListing()
1089 #self.updateDbusHandler.ArticleCountUpdated()
1091 def onFeedClosedTimeout(self):
1092 self.listing.saveConfig()
1094 self.updateDbusHandler.ArticleCountUpdated()
1097 self.window.connect("destroy", gtk.main_quit)
1099 self.listing.saveConfig()
1102 def prefsClosed(self, *widget):
1104 self.orientation.set_mode(self.config.getOrientation())
1107 self.displayListing()
1108 self.checkAutoUpdate()
1110 def checkAutoUpdate(self, *widget):
1111 interval = int(self.config.getUpdateInterval()*3600000)
1112 if self.config.isAutoUpdateEnabled():
1113 if self.autoupdate == False:
1114 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1115 self.autoupdate = interval
1116 elif not self.autoupdate == interval:
1117 # If auto-update is enabled, but not at the right frequency
1118 gobject.source_remove(self.autoupdateId)
1119 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1120 self.autoupdate = interval
1122 if not self.autoupdate == False:
1123 gobject.source_remove(self.autoupdateId)
1124 self.autoupdate = False
1126 def automaticUpdate(self, *widget):
1127 # Need to check for internet connection
1128 # If no internet connection, try again in 10 minutes:
1129 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1130 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1131 #from time import localtime, strftime
1132 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1134 self.button_update_clicked(None, None)
1137 def stopUpdate(self):
1138 # Not implemented in the app (see update_feeds.py)
1140 self.downloadDialog.listOfKeys = []
1144 def getStatus(self):
1146 for key in self.listing.getListOfFeeds():
1147 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1148 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1150 status = "No unread items"
1153 if __name__ == "__main__":
1154 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1155 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1156 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1157 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1158 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1159 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1160 gobject.threads_init()
1161 if not isdir(CONFIGDIR):
1165 print "Error: Can't create configuration directory"
1166 from sys import exit