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)
235 opener = build_opener()
237 opener.addheaders = [('User-agent', USER_AGENT)]
238 install_opener(opener)
241 self.set_text("Updating...")
243 self.set_fraction(self.fraction)
246 self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
248 def update_progress_bar(self):
249 #self.progress_bar.pulse()
250 if activeCount() < 4:
251 x = activeCount() - 1
252 k = len(self.listOfKeys)
253 fin = self.total - k - x
254 fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
255 #print x, k, fin, fraction
256 self.set_fraction(fraction)
258 if len(self.listOfKeys)>0:
259 self.current = self.current+1
260 key = self.listOfKeys.pop()
261 #if self.single == True:
262 # Check if the feed is being displayed
263 download = Download(self.listing, key, self.config)
266 elif activeCount() > 1:
269 #self.waitingWindow.destroy()
275 self.emit("download-done", "success")
280 class SortList(hildon.StackableWindow):
281 def __init__(self, parent, listing, feedingit, after_closing):
282 hildon.StackableWindow.__init__(self)
283 self.set_transient_for(parent)
284 self.set_title('Subscriptions')
285 self.listing = listing
286 self.feedingit = feedingit
287 self.after_closing = after_closing
288 self.connect('destroy', lambda w: self.after_closing())
289 self.vbox2 = gtk.VBox(False, 2)
291 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
292 button.set_image(gtk.image_new_from_icon_name('keyboard_move_up', gtk.ICON_SIZE_BUTTON))
293 button.connect("clicked", self.buttonUp)
294 self.vbox2.pack_start(button, expand=False, fill=False)
296 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
297 button.set_image(gtk.image_new_from_icon_name('keyboard_move_down', gtk.ICON_SIZE_BUTTON))
298 button.connect("clicked", self.buttonDown)
299 self.vbox2.pack_start(button, expand=False, fill=False)
301 self.vbox2.pack_start(gtk.Label(), expand=True, fill=False)
303 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
304 button.set_image(gtk.image_new_from_icon_name('general_add', gtk.ICON_SIZE_BUTTON))
305 button.connect("clicked", self.buttonAdd)
306 self.vbox2.pack_start(button, expand=False, fill=False)
308 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
309 button.set_image(gtk.image_new_from_icon_name('general_information', gtk.ICON_SIZE_BUTTON))
310 button.connect("clicked", self.buttonEdit)
311 self.vbox2.pack_start(button, expand=False, fill=False)
313 button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
314 button.set_image(gtk.image_new_from_icon_name('general_delete', gtk.ICON_SIZE_BUTTON))
315 button.connect("clicked", self.buttonDelete)
316 self.vbox2.pack_start(button, expand=False, fill=False)
318 #button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
319 #button.set_label("Done")
320 #button.connect("clicked", self.buttonDone)
321 #self.vbox.pack_start(button)
322 self.hbox2= gtk.HBox(False, 10)
323 self.pannableArea = hildon.PannableArea()
324 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
325 self.treeview = gtk.TreeView(self.treestore)
326 self.hbox2.pack_start(self.pannableArea, expand=True)
328 self.hbox2.pack_end(self.vbox2, expand=False)
329 self.set_default_size(-1, 600)
332 menu = hildon.AppMenu()
333 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
334 button.set_label("Import from OPML")
335 button.connect("clicked", self.feedingit.button_import_clicked)
338 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
339 button.set_label("Export to OPML")
340 button.connect("clicked", self.feedingit.button_export_clicked)
342 self.set_app_menu(menu)
346 #self.connect("destroy", self.buttonDone)
348 def displayFeeds(self):
349 self.treeview.destroy()
350 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
351 self.treeview = gtk.TreeView()
353 self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
354 hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
356 self.treeview.append_column(gtk.TreeViewColumn('Feed Name', gtk.CellRendererText(), text = 0))
358 self.pannableArea.add(self.treeview)
362 def refreshList(self, selected=None, offset=0):
363 #rect = self.treeview.get_visible_rect()
364 #y = rect.y+rect.height
365 self.treestore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
366 for key in self.listing.getListOfFeeds():
367 item = self.treestore.append([self.listing.getFeedTitle(key), key])
370 self.treeview.set_model(self.treestore)
371 if not selected == None:
372 self.treeview.get_selection().select_iter(selectedItem)
373 self.treeview.scroll_to_cell(self.treeview.get_model().get_path(selectedItem))
374 self.pannableArea.show_all()
376 def getSelectedItem(self):
377 (model, iter) = self.treeview.get_selection().get_selected()
380 return model.get_value(iter, 1)
382 def findIndex(self, key):
386 for row in self.treestore:
388 return (before, row.iter)
389 if key == list(row)[0]:
393 return (before, None)
395 def buttonUp(self, button):
396 key = self.getSelectedItem()
398 self.listing.moveUp(key)
399 self.refreshList(key, -10)
401 def buttonDown(self, button):
402 key = self.getSelectedItem()
404 self.listing.moveDown(key)
405 self.refreshList(key, 10)
407 def buttonDelete(self, button):
408 key = self.getSelectedItem()
410 self.listing.removeFeed(key)
413 def buttonEdit(self, button):
414 key = self.getSelectedItem()
416 wizard = AddWidgetWizard(self, self.listing.getFeedUrl(key), self.listing.getFeedTitle(key))
419 (title, url) = wizard.getData()
420 if (not title == '') and (not url == ''):
421 self.listing.editFeed(key, title, url)
425 def buttonDone(self, *args):
428 def buttonAdd(self, button, urlIn="http://"):
429 wizard = AddWidgetWizard(self, urlIn)
432 (title, url) = wizard.getData()
433 if (not title == '') and (not url == ''):
434 self.listing.addFeed(title, url)
439 class DisplayArticle(hildon.StackableWindow):
440 def __init__(self, feed, id, key, config, listing):
441 hildon.StackableWindow.__init__(self)
442 #self.imageDownloader = ImageDownloader()
447 #self.set_title(feed.getTitle(id))
448 self.set_title(self.listing.getFeedTitle(key))
450 self.set_for_removal = False
452 # Init the article display
453 #if self.config.getWebkitSupport():
454 self.view = WebView()
455 #self.view.set_editable(False)
458 # self.view = gtkhtml2.View()
459 # self.document = gtkhtml2.Document()
460 # self.view.set_document(self.document)
461 # self.document.connect("link_clicked", self._signal_link_clicked)
462 self.pannable_article = hildon.PannableArea()
463 self.pannable_article.add(self.view)
464 #self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
465 #self.gestureId = self.pannable_article.connect('horizontal-movement', self.gesture)
467 #if self.config.getWebkitSupport():
468 contentLink = self.feed.getContentLink(self.id)
469 self.feed.setEntryRead(self.id)
470 #if key=="ArchivedArticles":
471 if contentLink.startswith("/home/user/"):
472 self.view.open("file://" + contentLink)
474 self.view.load_html_string('This article has not been downloaded yet. Click <a href="%s">here</a> to view online.' % contentLink, contentLink)
475 self.view.connect("motion-notify-event", lambda w,ev: True)
476 self.view.connect('load-started', self.load_started)
477 self.view.connect('load-finished', self.load_finished)
480 #self.view.load_html_string(self.text, contentLink) # "text/html", "utf-8", self.link)
481 self.view.set_zoom_level(float(config.getArtFontSize())/10.)
483 # if not key == "ArchivedArticles":
484 # Do not download images if the feed is "Archived Articles"
485 # self.document.connect("request-url", self._signal_request_url)
487 # self.document.clear()
488 # self.document.open_stream("text/html")
489 # self.document.write_stream(self.text)
490 # self.document.close_stream()
492 menu = hildon.AppMenu()
493 # Create a button and add it to the menu
494 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
495 button.set_label("Allow horizontal scrolling")
496 button.connect("clicked", self.horiz_scrolling_button)
499 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
500 button.set_label("Open in browser")
501 button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
504 if key == "ArchivedArticles":
505 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
506 button.set_label("Remove from archived articles")
507 button.connect("clicked", self.remove_archive_button)
509 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
510 button.set_label("Add to archived articles")
511 button.connect("clicked", self.archive_button)
514 self.set_app_menu(menu)
517 #self.event_box = gtk.EventBox()
518 #self.event_box.add(self.pannable_article)
519 self.add(self.pannable_article)
522 self.pannable_article.show_all()
524 self.destroyId = self.connect("destroy", self.destroyWindow)
526 self.view.connect("button_press_event", self.button_pressed)
527 self.gestureId = self.view.connect("button_release_event", self.button_released)
528 #self.timeout_handler_id = gobject.timeout_add(300, self.reloadArticle)
530 def load_started(self, *widget):
531 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
533 def load_finished(self, *widget):
534 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
536 def button_pressed(self, window, event):
537 #print event.x, event.y
538 self.coords = (event.x, event.y)
540 def button_released(self, window, event):
541 x = self.coords[0] - event.x
542 y = self.coords[1] - event.y
544 if (2*abs(y) < abs(x)):
546 self.emit("article-previous", self.id)
548 self.emit("article-next", self.id)
552 #def gesture(self, widget, direction, startx, starty):
553 # if (direction == 3):
554 # self.emit("article-next", self.index)
555 # if (direction == 2):
556 # self.emit("article-previous", self.index)
557 #print startx, starty
558 #self.timeout_handler_id = gobject.timeout_add(200, self.destroyWindow)
560 def destroyWindow(self, *args):
561 self.disconnect(self.destroyId)
562 if self.set_for_removal:
563 self.emit("article-deleted", self.id)
565 self.emit("article-closed", self.id)
566 #self.imageDownloader.stopAll()
569 def horiz_scrolling_button(self, *widget):
570 self.pannable_article.disconnect(self.gestureId)
571 self.pannable_article.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
573 def archive_button(self, *widget):
574 # Call the listing.addArchivedArticle
575 self.listing.addArchivedArticle(self.key, self.id)
577 def remove_archive_button(self, *widget):
578 self.set_for_removal = True
580 #def reloadArticle(self, *widget):
581 # if threading.activeCount() > 1:
582 # Image thread are still running, come back in a bit
585 # for (stream, imageThread) in self.images:
587 # stream.write(imageThread.data)
592 def _signal_link_clicked(self, object, link):
594 bus = dbus.SessionBus()
595 proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
596 iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
597 iface.open_new_window(link)
599 #def _signal_request_url(self, object, url, stream):
601 # self.imageDownloader.queueImage(url, stream)
602 #imageThread = GetImage(url)
604 #self.images.append((stream, imageThread))
607 class DisplayFeed(hildon.StackableWindow):
608 def __init__(self, listing, feed, title, key, config, updateDbusHandler):
609 hildon.StackableWindow.__init__(self)
610 self.listing = listing
612 self.feedTitle = title
613 self.set_title(title)
616 self.updateDbusHandler = updateDbusHandler
618 self.downloadDialog = False
620 #self.listing.setCurrentlyDisplayedFeed(self.key)
624 menu = hildon.AppMenu()
625 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
626 button.set_label("Update feed")
627 button.connect("clicked", self.button_update_clicked)
630 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
631 button.set_label("Mark all as read")
632 button.connect("clicked", self.buttonReadAllClicked)
635 if key=="ArchivedArticles":
636 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
637 button.set_label("Delete read articles")
638 button.connect("clicked", self.buttonPurgeArticles)
641 self.set_app_menu(menu)
646 self.connect('configure-event', self.on_configure_event)
647 self.connect("destroy", self.destroyWindow)
649 def on_configure_event(self, window, event):
650 if getattr(self, 'markup_renderer', None) is None:
653 # Fix up the column width for wrapping the text when the window is
654 # resized (i.e. orientation changed)
655 self.markup_renderer.set_property('wrap-width', event.width-10)
657 def destroyWindow(self, *args):
658 #self.feed.saveUnread(CONFIGDIR)
659 gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
660 self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
661 self.emit("feed-closed", self.key)
663 #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
664 #self.listing.closeCurrentlyDisplayedFeed()
666 def fix_title(self, title):
667 return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
669 def displayFeed(self):
670 self.pannableFeed = hildon.PannableArea()
672 self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
674 self.feedItems = gtk.ListStore(str, str)
675 #self.feedList = gtk.TreeView(self.feedItems)
676 self.feedList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
677 selection = self.feedList.get_selection()
678 selection.set_mode(gtk.SELECTION_NONE)
679 #selection.connect("changed", lambda w: True)
681 self.feedList.set_model(self.feedItems)
682 self.feedList.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)
685 self.feedList.set_hover_selection(False)
686 #self.feedList.set_property('enable-grid-lines', True)
687 #self.feedList.set_property('hildon-mode', 1)
688 #self.pannableFeed.connect("motion-notify-event", lambda w,ev: True)
690 #self.feedList.connect('row-activated', self.on_feedList_row_activated)
692 vbox= gtk.VBox(False, 10)
693 vbox.pack_start(self.feedList)
695 self.pannableFeed.add_with_viewport(vbox)
697 self.markup_renderer = gtk.CellRendererText()
698 self.markup_renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR)
699 self.markup_renderer.set_property('wrap-width', 780)
700 self.markup_renderer.set_property('ypad', 5)
701 self.markup_renderer.set_property('xpad', 5)
702 markup_column = gtk.TreeViewColumn('', self.markup_renderer, \
703 markup=FEED_COLUMN_MARKUP)
704 self.feedList.append_column(markup_column)
706 #self.pannableFeed.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
707 hideReadArticles = self.config.getHideReadArticles()
709 for id in self.feed.getIds():
712 isRead = self.feed.isEntryRead(id)
715 if not ( isRead and hideReadArticles ):
716 #if not ( self.feed.isEntryRead(id) and self.config.getHideReadArticles() ):
717 #title = self.feed.getTitle(id)
718 title = self.fix_title(self.feed.getTitle(id))
720 #if self.feed.isEntryRead(id):
722 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
724 markup = ENTRY_TEMPLATE_UNREAD % (self.config.getFontSize(), title)
726 self.feedItems.append((markup, id))
729 self.feedList.connect('hildon-row-tapped', self.on_feedList_row_activated)
731 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
732 self.feedItems.append((markup, ""))
734 self.add(self.pannableFeed)
738 self.pannableFeed.destroy()
739 #self.remove(self.pannableFeed)
741 def on_feedList_row_activated(self, treeview, path): #, column):
742 selection = self.feedList.get_selection()
743 selection.set_mode(gtk.SELECTION_SINGLE)
744 self.feedList.get_selection().select_path(path)
745 model = treeview.get_model()
746 iter = model.get_iter(path)
747 key = model.get_value(iter, FEED_COLUMN_KEY)
748 # Emulate legacy "button_clicked" call via treeview
749 gobject.idle_add(self.button_clicked, treeview, key)
752 def button_clicked(self, button, index, previous=False, next=False):
753 #newDisp = DisplayArticle(self.feedTitle, self.feed.getArticle(index), self.feed.getLink(index), index, self.key, self.listing, self.config)
754 newDisp = DisplayArticle(self.feed, index, self.key, self.config, self.listing)
755 stack = hildon.WindowStack.get_default()
758 stack.pop_and_push(1, newDisp, tmp)
760 gobject.timeout_add(200, self.destroyArticle, tmp)
765 if type(self.disp).__name__ == "DisplayArticle":
766 gobject.timeout_add(200, self.destroyArticle, self.disp)
773 if self.key == "ArchivedArticles":
774 self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
775 self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
776 self.ids.append(self.disp.connect("article-next", self.nextArticle))
777 self.ids.append(self.disp.connect("article-previous", self.previousArticle))
779 def buttonPurgeArticles(self, *widget):
781 self.feed.purgeReadArticles()
782 self.feed.saveUnread(CONFIGDIR)
783 self.feed.saveFeed(CONFIGDIR)
786 def destroyArticle(self, handle):
787 handle.destroyWindow()
789 def mark_item_read(self, key):
790 it = self.feedItems.get_iter_first()
791 while it is not None:
792 k = self.feedItems.get_value(it, FEED_COLUMN_KEY)
794 title = self.fix_title(self.feed.getTitle(key))
795 markup = ENTRY_TEMPLATE % (self.config.getFontSize(), title)
796 self.feedItems.set_value(it, FEED_COLUMN_MARKUP, markup)
798 it = self.feedItems.iter_next(it)
800 def nextArticle(self, object, index):
801 self.mark_item_read(index)
802 id = self.feed.getNextId(index)
803 if self.config.getHideReadArticles():
806 isRead = self.feed.isEntryRead(id)
809 while isRead and id != index:
810 id = self.feed.getNextId(id)
813 isRead = self.feed.isEntryRead(id)
817 self.button_clicked(object, id, next=True)
819 def previousArticle(self, object, index):
820 self.mark_item_read(index)
821 id = self.feed.getPreviousId(index)
822 if self.config.getHideReadArticles():
825 isRead = self.feed.isEntryRead(id)
828 while isRead and id != index:
829 id = self.feed.getPreviousId(id)
832 isRead = self.feed.isEntryRead(id)
836 self.button_clicked(object, id, previous=True)
838 def onArticleClosed(self, object, index):
839 selection = self.feedList.get_selection()
840 selection.set_mode(gtk.SELECTION_NONE)
841 self.mark_item_read(index)
843 def onArticleDeleted(self, object, index):
845 self.feed.removeArticle(index)
846 self.feed.saveUnread(CONFIGDIR)
847 self.feed.saveFeed(CONFIGDIR)
850 def button_update_clicked(self, button):
851 #bar = DownloadBar(self, self.listing, [self.key,], self.config )
852 if not type(self.downloadDialog).__name__=="DownloadBar":
853 self.pannableFeed.destroy()
854 self.vbox = gtk.VBox(False, 10)
855 self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
856 self.downloadDialog.connect("download-done", self.onDownloadsDone)
857 self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
861 def onDownloadsDone(self, *widget):
863 self.feed = self.listing.getFeed(self.key)
865 self.updateDbusHandler.ArticleCountUpdated()
867 def buttonReadAllClicked(self, button):
868 for index in self.feed.getIds():
869 self.feed.setEntryRead(index)
870 self.mark_item_read(index)
876 self.window = hildon.StackableWindow()
877 self.window.set_title(__appname__)
878 hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
879 self.mainVbox = gtk.VBox(False,10)
881 self.introLabel = gtk.Label("Loading...")
884 self.mainVbox.pack_start(self.introLabel)
886 self.window.add(self.mainVbox)
887 self.window.show_all()
888 self.config = Config(self.window, CONFIGDIR+"config.ini")
889 gobject.idle_add(self.createWindow)
891 def createWindow(self):
892 self.app_lock = get_lock("app_lock")
893 if self.app_lock == None:
894 self.introLabel.set_label("Update in progress, please wait.")
895 gobject.timeout_add_seconds(3, self.createWindow)
897 self.listing = Listing(CONFIGDIR)
899 self.downloadDialog = False
901 self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
902 self.orientation.set_mode(self.config.getOrientation())
904 print "Could not start rotation manager"
906 menu = hildon.AppMenu()
907 # Create a button and add it to the menu
908 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
909 button.set_label("Update feeds")
910 button.connect("clicked", self.button_update_clicked, "All")
913 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
914 button.set_label("Mark all as read")
915 button.connect("clicked", self.button_markAll)
918 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
919 button.set_label("Manage subscriptions")
920 button.connect("clicked", self.button_organize_clicked)
923 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
924 button.set_label("Settings")
925 button.connect("clicked", self.button_preferences_clicked)
928 button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
929 button.set_label("About")
930 button.connect("clicked", self.button_about_clicked)
933 self.window.set_app_menu(menu)
936 #self.feedWindow = hildon.StackableWindow()
937 #self.articleWindow = hildon.StackableWindow()
938 self.introLabel.destroy()
939 self.pannableListing = hildon.PannableArea()
940 self.feedItems = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
941 self.feedList = gtk.TreeView(self.feedItems)
942 self.feedList.connect('row-activated', self.on_feedList_row_activated)
943 self.pannableListing.add(self.feedList)
945 icon_renderer = gtk.CellRendererPixbuf()
946 icon_renderer.set_property('width', LIST_ICON_SIZE + 2*LIST_ICON_BORDER)
947 icon_column = gtk.TreeViewColumn('', icon_renderer, \
949 self.feedList.append_column(icon_column)
951 markup_renderer = gtk.CellRendererText()
952 markup_column = gtk.TreeViewColumn('', markup_renderer, \
953 markup=COLUMN_MARKUP)
954 self.feedList.append_column(markup_column)
955 self.mainVbox.pack_start(self.pannableListing)
956 self.mainVbox.show_all()
958 self.displayListing()
959 self.autoupdate = False
960 self.checkAutoUpdate()
961 hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
962 gobject.idle_add(self.enableDbus)
964 def enableDbus(self):
965 self.dbusHandler = ServerObject(self)
966 self.updateDbusHandler = UpdateServerObject(self)
968 def button_markAll(self, button):
969 for key in self.listing.getListOfFeeds():
970 feed = self.listing.getFeed(key)
971 for id in feed.getIds():
972 feed.setEntryRead(id)
973 feed.saveUnread(CONFIGDIR)
974 self.listing.updateUnread(key, feed.getNumberOfUnreadItems())
975 self.displayListing()
977 def button_about_clicked(self, button):
978 HeAboutDialog.present(self.window, \
988 def button_export_clicked(self, button):
989 opml = ExportOpmlData(self.window, self.listing)
991 def button_import_clicked(self, button):
992 opml = GetOpmlData(self.window)
993 feeds = opml.getData()
994 for (title, url) in feeds:
995 self.listing.addFeed(title, url)
996 self.displayListing()
998 def addFeed(self, urlIn="http://"):
999 wizard = AddWidgetWizard(self.window, urlIn)
1002 (title, url) = wizard.getData()
1003 if (not title == '') and (not url == ''):
1004 self.listing.addFeed(title, url)
1006 self.displayListing()
1008 def button_organize_clicked(self, button):
1009 def after_closing():
1010 self.listing.saveConfig()
1011 self.displayListing()
1012 SortList(self.window, self.listing, self, after_closing)
1014 def button_update_clicked(self, button, key):
1015 if not type(self.downloadDialog).__name__=="DownloadBar":
1016 self.updateDbusHandler.UpdateStarted()
1017 self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
1018 self.downloadDialog.connect("download-done", self.onDownloadsDone)
1019 self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
1020 self.mainVbox.show_all()
1021 #self.displayListing()
1023 def onDownloadsDone(self, *widget):
1024 self.downloadDialog.destroy()
1025 self.downloadDialog = False
1026 self.displayListing()
1027 self.updateDbusHandler.UpdateFinished()
1028 self.updateDbusHandler.ArticleCountUpdated()
1030 def button_preferences_clicked(self, button):
1031 dialog = self.config.createDialog()
1032 dialog.connect("destroy", self.prefsClosed)
1034 def show_confirmation_note(self, parent, title):
1035 note = hildon.Note("confirmation", parent, "Are you sure you want to delete " + title +"?")
1037 retcode = gtk.Dialog.run(note)
1040 if retcode == gtk.RESPONSE_OK:
1045 def displayListing(self):
1046 icon_theme = gtk.icon_theme_get_default()
1047 default_pixbuf = icon_theme.load_icon(ABOUT_ICON, LIST_ICON_SIZE, \
1048 gtk.ICON_LOOKUP_USE_BUILTIN)
1050 self.feedItems.clear()
1051 for key in self.listing.getListOfFeeds():
1052 unreadItems = self.listing.getFeedNumberOfUnreadItems(key)
1053 if unreadItems > 0 or not self.config.getHideReadFeeds():
1054 title = self.listing.getFeedTitle(key)
1055 updateTime = self.listing.getFeedUpdateTime(key)
1057 subtitle = '%s / %d unread items' % (updateTime, unreadItems)
1060 markup = FEED_TEMPLATE_UNREAD % (title, subtitle)
1062 markup = FEED_TEMPLATE % (title, subtitle)
1065 icon_filename = self.listing.getFavicon(key)
1066 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, \
1067 LIST_ICON_SIZE, LIST_ICON_SIZE)
1069 pixbuf = default_pixbuf
1071 self.feedItems.append((pixbuf, markup, key))
1073 def on_feedList_row_activated(self, treeview, path, column):
1074 model = treeview.get_model()
1075 iter = model.get_iter(path)
1076 key = model.get_value(iter, COLUMN_KEY)
1079 def openFeed(self, key):
1083 # If feed_lock doesn't exist, we can open the feed, else we do nothing
1084 self.feed_lock = get_lock(key)
1085 self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
1086 self.listing.getFeedTitle(key), key, \
1087 self.config, self.updateDbusHandler)
1088 self.disp.connect("feed-closed", self.onFeedClosed)
1091 def onFeedClosed(self, object, key):
1092 #self.listing.saveConfig()
1094 gobject.idle_add(self.onFeedClosedTimeout)
1095 self.displayListing()
1096 #self.updateDbusHandler.ArticleCountUpdated()
1098 def onFeedClosedTimeout(self):
1099 self.listing.saveConfig()
1101 self.updateDbusHandler.ArticleCountUpdated()
1104 self.window.connect("destroy", gtk.main_quit)
1106 self.listing.saveConfig()
1109 def prefsClosed(self, *widget):
1111 self.orientation.set_mode(self.config.getOrientation())
1114 self.displayListing()
1115 self.checkAutoUpdate()
1117 def checkAutoUpdate(self, *widget):
1118 interval = int(self.config.getUpdateInterval()*3600000)
1119 if self.config.isAutoUpdateEnabled():
1120 if self.autoupdate == False:
1121 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1122 self.autoupdate = interval
1123 elif not self.autoupdate == interval:
1124 # If auto-update is enabled, but not at the right frequency
1125 gobject.source_remove(self.autoupdateId)
1126 self.autoupdateId = gobject.timeout_add(interval, self.automaticUpdate)
1127 self.autoupdate = interval
1129 if not self.autoupdate == False:
1130 gobject.source_remove(self.autoupdateId)
1131 self.autoupdate = False
1133 def automaticUpdate(self, *widget):
1134 # Need to check for internet connection
1135 # If no internet connection, try again in 10 minutes:
1136 # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
1137 #file = open("/home/user/.feedingit/feedingit_widget.log", "a")
1138 #from time import localtime, strftime
1139 #file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
1141 self.button_update_clicked(None, None)
1144 def stopUpdate(self):
1145 # Not implemented in the app (see update_feeds.py)
1147 self.downloadDialog.listOfKeys = []
1151 def getStatus(self):
1153 for key in self.listing.getListOfFeeds():
1154 if self.listing.getFeedNumberOfUnreadItems(key) > 0:
1155 status += self.listing.getFeedTitle(key) + ": \t" + str(self.listing.getFeedNumberOfUnreadItems(key)) + " Unread Items\n"
1157 status = "No unread items"
1160 if __name__ == "__main__":
1161 gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1162 gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1163 gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1164 gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1165 gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1166 gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
1167 gobject.threads_init()
1168 if not isdir(CONFIGDIR):
1172 print "Error: Can't create configuration directory"
1173 from sys import exit