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>'
87 # Build the markup template for the Maemo 5 text style
88 head_font = style.get_font_desc('SystemFont')
89 sub_font = style.get_font_desc('SmallSystemFont')
91 head_color = style.get_color('ButtonTextColor')
92 sub_color = style.get_color('DefaultTextColor')
93 active_color = style.get_color('ActiveTextColor')
95 head = MARKUP_TEMPLATE % (head_font.to_string(), head_color.to_string())
96 normal_sub = MARKUP_TEMPLATE % (sub_font.to_string(), sub_color.to_string())
98 active_head = MARKUP_TEMPLATE % (head_font.to_string(), active_color.to_string())
99 active_sub = MARKUP_TEMPLATE % (sub_font.to_string(), active_color.to_string())
101 FEED_TEMPLATE = '\n'.join((head, normal_sub))
102 FEED_TEMPLATE_UNREAD = '\n'.join((head, active_sub))
104 ENTRY_TEMPLATE = head
105 ENTRY_TEMPLATE_UNREAD = active_head
108 # Removes HTML or XML character references and entities from a text string.
110 # @param text The HTML (or XML) source text.
111 # @return The plain text, as a Unicode string, if necessary.
112 # http://effbot.org/zone/re-sub.htm#unescape-html
117 # character reference
119 if text[:3] == "&#x":
120 return unichr(int(text[3:-1], 16))
122 return unichr(int(text[2:-1]))
128 text = unichr(name2codepoint[text[1:-1]])
131 return text # leave as is
132 return sub("&#?\w+;", fixup, text)
135 class AddWidgetWizard(hildon.WizardDialog):
137 def __init__(self, parent, urlIn, titleIn=None):
139 self.notebook = gtk.Notebook()
141 self.nameEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
142 self.nameEntry.set_placeholder("Enter Feed Name")
143 vbox = gtk.VBox(False,10)
144 label = gtk.Label("Enter Feed Name:")
145 vbox.pack_start(label)
146 vbox.pack_start(self.nameEntry)
147 if not titleIn == None:
148 self.nameEntry.set_text(titleIn)
149 self.notebook.append_page(vbox, None)
151 self.urlEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
152 self.urlEntry.set_placeholder("Enter a URL")
153 self.urlEntry.set_text(urlIn)
154 self.urlEntry.select_region(0,-1)
156 vbox = gtk.VBox(False,10)
157 label = gtk.Label("Enter Feed URL:")
158 vbox.pack_start(label)
159 vbox.pack_start(self.urlEntry)
160 self.notebook.append_page(vbox, None)
162 labelEnd = gtk.Label("Success")
164 self.notebook.append_page(labelEnd, None)
166 hildon.WizardDialog.__init__(self, parent, "Add Feed", self.notebook)
168 # Set a handler for "switch-page" signal
169 #self.notebook.connect("switch_page", self.on_page_switch, self)
171 # Set a function to decide if user can go to next page
172 self.set_forward_page_func(self.some_page_func)
177 return (self.nameEntry.get_text(), self.urlEntry.get_text())
179 def on_page_switch(self, notebook, page, num, dialog):
182 def some_page_func(self, nb, current, userdata):
183 # Validate data for 1st page
185 return len(self.nameEntry.get_text()) != 0
187 # Check the url is not null, and starts with http
188 return ( (len(self.urlEntry.get_text()) != 0) and (self.urlEntry.get_text().lower().startswith("http")) )
194 class Download(Thread):
195 def __init__(self, listing, key, config):
196 Thread.__init__(self)
197 self.listing = listing
202 (use_proxy, proxy) = self.config.getProxy()
203 key_lock = get_lock(self.key)
206 self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
208 self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
212 class DownloadBar(gtk.ProgressBar):
213 def __init__(self, parent, listing, listOfKeys, config, single=False):
215 update_lock = get_lock("update_lock")
216 if update_lock != None:
217 gtk.ProgressBar.__init__(self)
218 self.listOfKeys = listOfKeys[:]
219 self.listing = listing
220 self.total = len(self.listOfKeys)
224 (use_proxy, proxy) = self.config.getProxy()
226 opener = build_opener(proxy)
227 opener.addheaders = [('User-agent', USER_AGENT)]
228 install_opener(opener)
230 opener = build_opener()
231 opener.addheaders = [('User-agent', USER_AGENT)]
232 install_opener(opener)
235 self.set_text("Updating...")
237 self.set_fraction(self.fraction)
240 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
242 def update_progress_bar(self):
243 #self.progress_bar.pulse()
244 if activeCount() < 4:
245 x = activeCount() - 1
246 k = len(self.listOfKeys)
247 fin = self.total - k - x
248 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
249 #print x, k, fin, fraction
250 self.set_fraction(fraction)
252 if len(self.listOfKeys)>0:
253 self.current = self.current+1
254 key = self.listOfKeys.pop()
255 #if self.single == True:
256 # Check if the feed is being displayed
257 download = Download(self.listing, key, self.config)
260 elif activeCount() > 1:
263 #self.waitingWindow.destroy()
269 self.emit("download-done", "success")
274 class SortList(gtk.Dialog):
275 def __init__(self, parent, listing):
276 gtk.Dialog.__init__(self, "Organizer", parent)
277 self.listing = listing
279 self.vbox2 = gtk.VBox(False, 10)
281 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
282 button.set_label("Move Up")
283 button.connect("clicked", self.buttonUp)
284 self.vbox2.pack_start(button, expand=False, fill=False)
286 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
287 button.set_label("Move Down")
288 button.connect("clicked", self.buttonDown)
289 self.vbox2.pack_start(button, expand=False, fill=False)
291 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
292 button.set_label("Add Feed")
293 button.connect("clicked", self.buttonAdd)
294 self.vbox2.pack_start(button, expand=False, fill=False)
296 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
297 button.set_label("Edit Feed")
298 button.connect("clicked", self.buttonEdit)
299 self.vbox2.pack_start(button, expand=False, fill=False)
301 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
302 button.set_label("Delete")
303 button.connect("clicked", self.buttonDelete)
304 self.vbox2.pack_start(button, expand=False, fill=False)
306 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
307 #button.set_label("Done")
308 #button.connect("clicked", self.buttonDone)
309 #self.vbox.pack_start(button)
310 self.hbox2= gtk.HBox(False, 10)
311 self.pannableArea = hildon.PannableArea()
312 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
313 self.treeview = gtk.TreeView(self.treestore)
314 self.hbox2.pack_start(self.pannableArea, expand=True)
316 self.hbox2.pack_end(self.vbox2, expand=False)
317 self.set_default_size(-1, 600)
318 self.vbox.pack_start(self.hbox2)
321 #self.connect("destroy", self.buttonDone)
323 def displayFeeds(self):
324 self.treeview.destroy()
325 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
326 self.treeview = gtk.TreeView()
328 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
329 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
331 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
333 self.pannableArea.add(self.treeview)
337 def refreshList(self, selected=None, offset=0):
338 #rect = self.treeview.get_visible_rect()
339 #y = rect.y+rect.height
340 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
341 for key in self.listing.getListOfFeeds():
342 item = self.treestore.append([self.listing.getFeedTitle(key), key])
345 self.treeview.set_model(self.treestore)
346 if not selected == None:
347 self.treeview.get_selection().select_iter(selectedItem)
348 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
349 self.pannableArea.show_all()
351 def getSelectedItem(self):
352 (model, iter) = self.treeview.get_selection().get_selected()
355 return model.get_value(iter, 1)
357 def findIndex(self, key):
361 for row in self.treestore:
363 return (before, row.iter)
364 if key == list(row)[0]:
368 return (before, None)
370 def buttonUp(self, button):
371 key = self.getSelectedItem()
373 self.listing.moveUp(key)
374 self.refreshList(key, -10)
376 def buttonDown(self, button):
377 key = self.getSelectedItem()
379 self.listing.moveDown(key)
380 self.refreshList(key, 10)
382 def buttonDelete(self, button):
383 key = self.getSelectedItem()
385 self.listing.removeFeed(key)
388 def buttonEdit(self, button):
389 key = self.getSelectedItem()
391 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
394 (title, url) = wizard.getData()
395 if (not title == '') and (not url == ''):
396 self.listing.editFeed(key, title, url)
400 def buttonDone(self, *args):
403 def buttonAdd(self, button, urlIn="http://"):
404 wizard = AddWidgetWizard(self, urlIn)
407 (title, url) = wizard.getData()
408 if (not title == '') and (not url == ''):
409 self.listing.addFeed(title, url)
414 class DisplayArticle(hildon.StackableWindow):
415 def __init__(self, feed, id, key, config, listing):
416 hildon.StackableWindow.__init__(self)
417 #self.imageDownloader = ImageDownloader()
422 #self.set_title(feed.getTitle(id))
423 self.set_title(self.listing.getFeedTitle(key))
425 self.set_for_removal = False
427 # Init the article display
428 #if self.config.getWebkitSupport():
429 self.view = WebView()
430 #self.view.set_editable(False)
433 # self.view = gtkhtml2.View()
434 # self.document = gtkhtml2.Document()
435 # self.view.set_document(self.document)
436 # self.document.connect("link_clicked", self._signal_link_clicked)
437 self.pannable_article = hildon.PannableArea()
438 self.pannable_article.add(self.view)
439 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
440 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
442 #if self.config.getWebkitSupport():
443 contentLink = self.feed.getContentLink(self.id)
444 self.feed.setEntryRead(self.id)
445 #if key=="ArchivedArticles":
446 if contentLink.startswith("/home/user/"):
447 self.view.open("file://" + contentLink)
449 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
450 self.view.connect("motion-notify-event", lambda w,ev: True)
451 self.view.connect('load-started', self.load_started)
452 self.view.connect('load-finished', self.load_finished)
455 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
456 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
458 # if not key == "ArchivedArticles":
459 # Do not download images if the feed is "Archived Articles"
460 # self.document.connect("request-url", self._signal_request_url)
462 # self.document.clear()
463 # self.document.open_stream("text/html")
464 # self.document.write_stream(self.text)
465 # self.document.close_stream()
467 menu = hildon.AppMenu()
468 # Create a button and add it to the menu
469 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
470 button.set_label("Allow Horizontal Scrolling")
471 button.connect("clicked", self.horiz_scrolling_button)
474 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
475 button.set_label("Open in Browser")
476 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
479 if key == "ArchivedArticles":
480 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
481 button.set_label("Remove from Archived Articles")
482 button.connect("clicked", self.remove_archive_button)
484 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
485 button.set_label("Add to Archived Articles")
486 button.connect("clicked", self.archive_button)
489 self.set_app_menu(menu)
492 #self.event_box = gtk.EventBox()
493 #self.event_box.add(self.pannable_article)
494 self.add(self.pannable_article)
497 self.pannable_article.show_all()
499 self.destroyId = self.connect("destroy", self.destroyWindow)
501 self.view.connect("button_press_event", self.button_pressed)
502 self.gestureId = self.view.connect("button_release_event", self.button_released)
503 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
505 def load_started(self, *widget):
506 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
508 def load_finished(self, *widget):
509 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
511 def button_pressed(self, window, event):
512 #print event.x, event.y
513 self.coords = (event.x, event.y)
515 def button_released(self, window, event):
516 x = self.coords[0] - event.x
517 y = self.coords[1] - event.y
519 if (2*abs(y) < abs(x)):
521 self.emit("article-previous", self.id)
523 self.emit("article-next", self.id)
527 #def gesture(self, widget, direction, startx, starty):
528 # if (direction == 3):
529 # self.emit("article-next", self.index)
530 # if (direction == 2):
531 # self.emit("article-previous", self.index)
532 #print startx, starty
533 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
535 def destroyWindow(self, *args):
536 self.disconnect(self.destroyId)
537 if self.set_for_removal:
538 self.emit("article-deleted", self.id)
540 self.emit("article-closed", self.id)
541 #self.imageDownloader.stopAll()
544 def horiz_scrolling_button(self, *widget):
545 self.pannable_article.disconnect(self.gestureId)
546 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
548 def archive_button(self, *widget):
549 # Call the listing.addArchivedArticle
550 self.listing.addArchivedArticle(self.key, self.id)
552 def remove_archive_button(self, *widget):
553 self.set_for_removal = True
555 #def reloadArticle(self, *widget):
556 # if threading.activeCount() > 1:
557 # Image thread are still running, come back in a bit
560 # for (stream, imageThread) in self.images:
562 # stream.write(imageThread.data)
567 def _signal_link_clicked(self, object, link):
569 bus = dbus.SessionBus()
570 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
571 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
572 iface.open_new_window(link)
574 #def _signal_request_url(self, object, url, stream):
576 # self.imageDownloader.queueImage(url, stream)
577 #imageThread = GetImage(url)
579 #self.images.append((stream, imageThread))
582 class DisplayFeed(hildon.StackableWindow):
583 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
584 hildon.StackableWindow.__init__(self)
585 self.listing = listing
587 self.feedTitle = title
588 self.set_title(title)
591 self.updateDbusHandler = updateDbusHandler
593 self.downloadDialog = False
595 #self.listing.setCurrentlyDisplayedFeed(self.key)
599 menu = hildon.AppMenu()
600 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
601 button.set_label("Update Feed")
602 button.connect("clicked", self.button_update_clicked)
605 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
606 button.set_label("Mark All As Read")
607 button.connect("clicked", self.buttonReadAllClicked)
610 if key=="ArchivedArticles":
611 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
612 button.set_label("Purge Read Articles")
613 button.connect("clicked", self.buttonPurgeArticles)
616 self.set_app_menu(menu)
621 self.connect('configure-event', self.on_configure_event)
622 self.connect("destroy", self.destroyWindow)
624 def on_configure_event(self, window, event):
625 if getattr(self, 'markup_renderer', None) is None:
628 # Fix up the column width for wrapping the text when the window is
629 # resized (i.e. orientation changed)
630 self.markup_renderer.set_property('wrap-width', event.width-10)
632 def destroyWindow(self, *args):
633 #self.feed.saveUnread(CONFIGDIR)
634 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
635 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
636 self.emit("feed-closed", self.key)
638 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
639 #self.listing.closeCurrentlyDisplayedFeed()
641 def fix_title(self, title):
642 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
644 def displayFeed(self):
645 self.pannableFeed = hildon.PannableArea()
647 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
649 self.feedItems = gtk.ListStore(str, str)
650 self.feedList = gtk.TreeView(self.feedItems)
651 self.feedList.connect('row-activated', self.on_feedList_row_activated)
652 self.pannableFeed.add(self.feedList)
654 self.markup_renderer = gtk.CellRendererText()
655 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
656 self.markup_renderer.set_property('wrap-width', 780)
657 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
658 markup=FEED_COLUMN_MARKUP)
659 self.feedList.append_column(markup_column)
661 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
663 for id in self.feed.getIds():
664 if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
665 #title = self.feed.getTitle(id)
666 title = self.fix_title(self.feed.getTitle(id))
668 if self.feed.isEntryRead(id):
669 markup = ENTRY_TEMPLATE % title
671 markup = ENTRY_TEMPLATE_UNREAD % title
673 self.feedItems.append((markup, id))
675 self.add(self.pannableFeed)
679 self.pannableFeed.destroy()
680 #self.remove(self.pannableFeed)
682 def on_feedList_row_activated(self, treeview, path, column):
683 model = treeview.get_model()
684 iter = model.get_iter(path)
685 key = model.get_value(iter, FEED_COLUMN_KEY)
686 # Emulate legacy "button_clicked" call via treeview
687 self.button_clicked(treeview, key)
689 def button_clicked(self, button, index, previous=False, next=False):
690 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
691 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
692 stack = hildon.WindowStack.get_default()
695 stack.pop_and_push(1, newDisp, tmp)
697 gobject.timeout_add(200, self.destroyArticle, tmp)
702 if type(self.disp).__name__ == "DisplayArticle":
703 gobject.timeout_add(200, self.destroyArticle, self.disp)
710 if self.key == "ArchivedArticles":
711 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
712 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
713 self.ids.append(self.disp.connect("article-next", self.nextArticle))
714 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
716 def buttonPurgeArticles(self, *widget):
718 self.feed.purgeReadArticles()
719 self.feed.saveUnread(CONFIGDIR)
720 self.feed.saveFeed(CONFIGDIR)
723 def destroyArticle(self, handle):
724 handle.destroyWindow()
726 def mark_item_read(self, key):
727 it = self.feedItems.get_iter_first()
728 while it is not None:
729 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
731 title = self.fix_title(self.feed.getTitle(key))
732 markup = ENTRY_TEMPLATE % title
733 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
735 it = self.feedItems.iter_next(it)
737 def nextArticle(self, object, index):
738 self.mark_item_read(index)
739 id = self.feed.getNextId(index)
740 self.button_clicked(object, id, next=True)
742 def previousArticle(self, object, index):
743 self.mark_item_read(index)
744 id = self.feed.getPreviousId(index)
745 self.button_clicked(object, id, previous=True)
747 def onArticleClosed(self, object, index):
748 self.mark_item_read(index)
750 def onArticleDeleted(self, object, index):
752 self.feed.removeArticle(index)
753 self.feed.saveUnread(CONFIGDIR)
754 self.feed.saveFeed(CONFIGDIR)
757 def button_update_clicked(self, button):
758 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
759 if not type(self.downloadDialog).__name__=="DownloadBar":
760 self.pannableFeed.destroy()
761 self.vbox = gtk.VBox(False, 10)
762 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
763 self.downloadDialog.connect("download-done", self.onDownloadsDone)
764 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
768 def onDownloadsDone(self, *widget):
770 self.feed = self.listing.getFeed(self.key)
772 self.updateDbusHandler.ArticleCountUpdated()
774 def buttonReadAllClicked(self, button):
775 for index in self.feed.getIds():
776 self.feed.setEntryRead(index)
777 self.mark_item_read(index)
783 self.window = hildon.StackableWindow()
784 self.window.set_title(__appname__)
785 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
786 self.mainVbox = gtk.VBox(False,10)
788 self.pannableListing = hildon.PannableArea()
789 self.mainVbox.pack_start(self.pannableListing)
791 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
792 self.feedList = gtk.TreeView(self.feedItems)
793 self.feedList.connect('row-activated', self.on_feedList_row_activated)
794 self.pannableListing.add(self.feedList)
796 icon_renderer = gtk.CellRendererPixbuf()
797 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
798 icon_column = gtk.TreeViewColumn('', icon_renderer, \
800 self.feedList.append_column(icon_column)
802 markup_renderer = gtk.CellRendererText()
803 markup_column = gtk.TreeViewColumn('', markup_renderer, \
804 markup=COLUMN_MARKUP)
805 self.feedList.append_column(markup_column)
807 self.window.add(self.mainVbox)
808 self.window.show_all()
809 self.config = Config(self.window, CONFIGDIR+"config.ini")
810 gobject.idle_add(self.createWindow)
812 def createWindow(self):
813 self.app_lock = get_lock("app_lock")
814 if self.app_lock == None:
815 self.pannableListing.set_label("Update in progress, please wait.")
816 gobject.timeout_add_seconds(3, self.createWindow)
818 self.listing = Listing(CONFIGDIR)
820 self.downloadDialog = False
822 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
823 self.orientation.set_mode(self.config.getOrientation())
825 print "Could not start rotation manager"
827 menu = hildon.AppMenu()
828 # Create a button and add it to the menu
829 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
830 button.set_label("Update All Feeds")
831 button.connect("clicked", self.button_update_clicked, "All")
834 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
835 button.set_label("Mark All As Read")
836 button.connect("clicked", self.button_markAll)
839 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
840 button.set_label("Organize Feeds")
841 button.connect("clicked", self.button_organize_clicked)
844 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
845 button.set_label("Preferences")
846 button.connect("clicked", self.button_preferences_clicked)
849 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
850 button.set_label("Import Feeds")
851 button.connect("clicked", self.button_import_clicked)
854 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
855 button.set_label("Export Feeds")
856 button.connect("clicked", self.button_export_clicked)
859 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
860 button.set_label("About")
861 button.connect("clicked", self.button_about_clicked)
864 self.window.set_app_menu(menu)
867 self.feedWindow = hildon.StackableWindow()
868 self.articleWindow = hildon.StackableWindow()
870 self.displayListing()
871 self.autoupdate = False
872 self.checkAutoUpdate()
873 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
874 gobject.idle_add(self.enableDbus)
876 def enableDbus(self):
877 self.dbusHandler = ServerObject(self)
878 self.updateDbusHandler = UpdateServerObject(self)
880 def button_markAll(self, button):
881 for key in self.listing.getListOfFeeds():
882 feed = self.listing.getFeed(key)
883 for id in feed.getIds():
884 feed.setEntryRead(id)
885 feed.saveUnread(CONFIGDIR)
886 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
887 self.displayListing()
889 def button_about_clicked(self, button):
890 HeAboutDialog.present(self.window, \
900 def button_export_clicked(self, button):
901 opml = ExportOpmlData(self.window, self.listing)
903 def button_import_clicked(self, button):
904 opml = GetOpmlData(self.window)
905 feeds = opml.getData()
906 for (title, url) in feeds:
907 self.listing.addFeed(title, url)
908 self.displayListing()
910 def addFeed(self, urlIn="http://"):
911 wizard = AddWidgetWizard(self.window, urlIn)
914 (title, url) = wizard.getData()
915 if (not title == '') and (not url == ''):
916 self.listing.addFeed(title, url)
918 self.displayListing()
920 def button_organize_clicked(self, button):
921 org = SortList(self.window, self.listing)
924 self.listing.saveConfig()
925 self.displayListing()
927 def button_update_clicked(self, button, key):
928 if not type(self.downloadDialog).__name__=="DownloadBar":
929 self.updateDbusHandler.UpdateStarted()
930 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
931 self.downloadDialog.connect("download-done", self.onDownloadsDone)
932 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
933 self.mainVbox.show_all()
934 #self.displayListing()
936 def onDownloadsDone(self, *widget):
937 self.downloadDialog.destroy()
938 self.downloadDialog = False
939 self.displayListing()
940 self.updateDbusHandler.UpdateFinished()
941 self.updateDbusHandler.ArticleCountUpdated()
943 def button_preferences_clicked(self, button):
944 dialog = self.config.createDialog()
945 dialog.connect("destroy", self.prefsClosed)
947 def show_confirmation_note(self, parent, title):
948 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
950 retcode = gtk.Dialog.run(note)
953 if retcode == gtk.RESPONSE_OK:
958 def displayListing(self):
959 icon_theme = gtk.icon_theme_get_default()
960 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
961 gtk.ICON_LOOKUP_USE_BUILTIN)
963 self.feedItems.clear()
964 for key in self.listing.getListOfFeeds():
965 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
966 if unreadItems > 0 or not self.config.getHideReadFeeds():
967 title = self.listing.getFeedTitle(key)
968 updateTime = self.listing.getFeedUpdateTime(key)
970 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
973 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
975 markup = FEED_TEMPLATE % (title, subtitle)
978 icon_filename = self.listing.getFavicon(key)
979 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
980 LIST_ICON_SIZE, LIST_ICON_SIZE)
982 pixbuf = default_pixbuf
984 self.feedItems.append((pixbuf, markup, key))
986 def on_feedList_row_activated(self, treeview, path, column):
987 model = treeview.get_model()
988 iter = model.get_iter(path)
989 key = model.get_value(iter, COLUMN_KEY)
994 # If feed_lock doesn't exist, we can open the feed, else we do nothing
995 self.feed_lock = get_lock(key)
996 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
997 self.listing.getFeedTitle(key), key, \
998 self.config, self.updateDbusHandler)
999 self.disp.connect("feed-closed", self.onFeedClosed)
1001 def onFeedClosed(self, object, key):
1002 #self.listing.saveConfig()
1004 gobject.idle_add(self.onFeedClosedTimeout)
1005 self.displayListing()
1006 #self.updateDbusHandler.ArticleCountUpdated()
1008 def onFeedClosedTimeout(self):
1009 self.listing.saveConfig()
1011 self.updateDbusHandler.ArticleCountUpdated()
1014 self.window.connect("destroy", gtk.main_quit)
1016 self.listing.saveConfig()
1019 def prefsClosed(self, *widget):
1020 self.orientation.set_mode(self.config.getOrientation())
1021 self.displayListing()
1022 self.checkAutoUpdate()
1024 def checkAutoUpdate(self, *widget):
1025 interval = int(self.config.getUpdateInterval()*3600000)
1026 if self.config.isAutoUpdateEnabled():
1027 if self.autoupdate == False:
1028 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1029 self.autoupdate = interval
1030 elif not self.autoupdate == interval:
1031 # If auto-update is enabled, but not at the right frequency
1032 gobject.source_remove(self.autoupdateId)
1033 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1034 self.autoupdate = interval
1036 if not self.autoupdate == False:
1037 gobject.source_remove(self.autoupdateId)
1038 self.autoupdate = False
1040 def automaticUpdate(self, *widget):
1041 # Need to check for internet connection
1042 # If no internet connection, try again in 10 minutes:
1043 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1044 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1045 #from time import localtime, strftime
1046 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1048 self.button_update_clicked(None, None)
1051 def stopUpdate(self):
1052 # Not implemented in the app (see update_feeds.py)
1054 self.downloadDialog.listOfKeys = []
1058 def getStatus(self):
1060 for key in self.listing.getListOfFeeds():
1061 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1062 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1064 status = "No unread items"
1067 if __name__ == "__main__":
1068 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1069 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1070 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1071 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1072 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1073 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1074 gobject.threads_init()
1075 if not isdir(CONFIGDIR):
1079 print "Error: Can't create configuration directory"
1080 from sys import exit